Merge ~twom/launchpad:oci-policy-set-the-official-with-the-official-permissions into launchpad:master
- Git
- lp:~twom/launchpad
- oci-policy-set-the-official-with-the-official-permissions
- Merge into master
Proposed by
Tom Wardill
Status: | Merged |
---|---|
Approved by: | Tom Wardill |
Approved revision: | a9792063cf939f4c8eabf14e0ee1f7e3ce53fa77 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~twom/launchpad:oci-policy-set-the-official-with-the-official-permissions |
Merge into: | launchpad:master |
Diff against target: |
634 lines (+272/-104) 9 files modified
lib/lp/oci/browser/ocirecipe.py (+63/-1) lib/lp/oci/browser/tests/test_ocirecipe.py (+178/-0) lib/lp/oci/templates/ocirecipe-index.pt (+7/-0) lib/lp/oci/templates/ocirecipe-new.pt (+3/-0) lib/lp/oci/tests/test_ocirecipe.py (+11/-10) lib/lp/registry/browser/ociproject.py (+0/-11) lib/lp/registry/browser/tests/test_ociproject.py (+2/-68) lib/lp/registry/interfaces/ociproject.py (+4/-5) lib/lp/registry/model/ociproject.py (+4/-9) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ioana Lasc (community) | Approve | ||
Thiago F. Pappacena (community) | Approve | ||
Review via email: mp+396584@code.launchpad.net |
Commit message
Set official status for an OCIRecipe via the UI
Description of the change
To post a comment you must log in.
- a53392c... by Tom Wardill
-
Tidy up old remnants
Revision history for this message
Tom Wardill (twom) : | # |
Revision history for this message
Thiago F. Pappacena (pappacena) : | # |
review:
Approve
- 3cb0b3f... by Tom Wardill
-
Remove official recipe options from OCI Project
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
LGTM. Added just a small comment on a test.
Now that we don't enforce a single official recipe per OCIProject, it seems a bit strange having OCIProject managing an attribute of OCIRecipe. But for me it's ok to keep it like this for now, and refactoring in the future.
- a979206... by Tom Wardill
-
Check the other status hasn't changed
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py | |||
2 | index a2b8119..7cf1f94 100644 | |||
3 | --- a/lib/lp/oci/browser/ocirecipe.py | |||
4 | +++ b/lib/lp/oci/browser/ocirecipe.py | |||
5 | @@ -742,6 +742,18 @@ class OCIRecipeFormMixin: | |||
6 | 742 | build_args[k] = v | 742 | build_args[k] = v |
7 | 743 | data['build_args'] = build_args | 743 | data['build_args'] = build_args |
8 | 744 | 744 | ||
9 | 745 | def userIsRecipeAdmin(self): | ||
10 | 746 | if check_permission("launchpad.Admin", self.context): | ||
11 | 747 | return True | ||
12 | 748 | person = getattr(self.request.principal, 'person', None) | ||
13 | 749 | if not person: | ||
14 | 750 | return False | ||
15 | 751 | # Edit context = OCIRecipe, New context = OCIProject | ||
16 | 752 | project = getattr(self.context, "oci_project", self.context) | ||
17 | 753 | if project.pillar.canAdministerOCIProjects(person): | ||
18 | 754 | return True | ||
19 | 755 | return False | ||
20 | 756 | |||
21 | 745 | 757 | ||
22 | 746 | class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, | 758 | class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, |
23 | 747 | OCIRecipeFormMixin): | 759 | OCIRecipeFormMixin): |
24 | @@ -775,6 +787,16 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, | |||
25 | 775 | "The architectures that this OCI recipe builds for. Some " | 787 | "The architectures that this OCI recipe builds for. Some " |
26 | 776 | "architectures are restricted and may only be enabled or " | 788 | "architectures are restricted and may only be enabled or " |
27 | 777 | "disabled by administrators.") | 789 | "disabled by administrators.") |
28 | 790 | self.form_fields += FormFields(Bool( | ||
29 | 791 | __name__="official_recipe", | ||
30 | 792 | title="Official recipe", | ||
31 | 793 | description=( | ||
32 | 794 | "Mark this recipe as official for this OCI Project. " | ||
33 | 795 | "Allows use of distribution registry credentials " | ||
34 | 796 | "and the default git repository routing. " | ||
35 | 797 | "May only be enabled by the owner of the OCI Project."), | ||
36 | 798 | default=False, | ||
37 | 799 | required=False, readonly=False)) | ||
38 | 778 | 800 | ||
39 | 779 | def setUpGitRefWidget(self): | 801 | def setUpGitRefWidget(self): |
40 | 780 | """Setup GitRef widget indicating the user to use the default | 802 | """Setup GitRef widget indicating the user to use the default |
41 | @@ -798,6 +820,11 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, | |||
42 | 798 | super(OCIRecipeAddView, self).setUpWidgets() | 820 | super(OCIRecipeAddView, self).setUpWidgets() |
43 | 799 | self.widgets["processors"].widget_class = "processors" | 821 | self.widgets["processors"].widget_class = "processors" |
44 | 800 | self.setUpGitRefWidget() | 822 | self.setUpGitRefWidget() |
45 | 823 | # disable the official recipe button if the user doesn't have | ||
46 | 824 | # permissions to change it | ||
47 | 825 | widget = self.widgets['official_recipe'] | ||
48 | 826 | if not self.userIsRecipeAdmin(): | ||
49 | 827 | widget.extra = "disabled='disabled'" | ||
50 | 801 | 828 | ||
51 | 802 | @property | 829 | @property |
52 | 803 | def cancel_url(self): | 830 | def cancel_url(self): |
53 | @@ -829,6 +856,12 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, | |||
54 | 829 | "this name." % ( | 856 | "this name." % ( |
55 | 830 | owner.display_name, self.context.display_name)) | 857 | owner.display_name, self.context.display_name)) |
56 | 831 | self.validateBuildArgs(data) | 858 | self.validateBuildArgs(data) |
57 | 859 | official = data.get("official_recipe", None) | ||
58 | 860 | if official and not self.userIsRecipeAdmin(): | ||
59 | 861 | self.setFieldError( | ||
60 | 862 | "official_recipe", | ||
61 | 863 | "You do not have permission to set the official status " | ||
62 | 864 | "of this recipe.") | ||
63 | 832 | 865 | ||
64 | 833 | @action("Create OCI recipe", name="create") | 866 | @action("Create OCI recipe", name="create") |
65 | 834 | def create_action(self, action, data): | 867 | def create_action(self, action, data): |
66 | @@ -837,7 +870,8 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, | |||
67 | 837 | oci_project=self.context, git_ref=data["git_ref"], | 870 | oci_project=self.context, git_ref=data["git_ref"], |
68 | 838 | build_file=data["build_file"], description=data["description"], | 871 | build_file=data["build_file"], description=data["description"], |
69 | 839 | build_daily=data["build_daily"], build_args=data["build_args"], | 872 | build_daily=data["build_daily"], build_args=data["build_args"], |
71 | 840 | build_path=data["build_path"], processors=data["processors"]) | 873 | build_path=data["build_path"], processors=data["processors"], |
72 | 874 | official=data.get('official_recipe', False)) | ||
73 | 841 | self.next_url = canonical_url(recipe) | 875 | self.next_url = canonical_url(recipe) |
74 | 842 | 876 | ||
75 | 843 | 877 | ||
76 | @@ -858,6 +892,11 @@ class BaseOCIRecipeEditView(LaunchpadEditFormView): | |||
77 | 858 | self.context.setProcessors( | 892 | self.context.setProcessors( |
78 | 859 | new_processors, check_permissions=True, user=self.user) | 893 | new_processors, check_permissions=True, user=self.user) |
79 | 860 | del data["processors"] | 894 | del data["processors"] |
80 | 895 | official = data.pop('official_recipe', None) | ||
81 | 896 | if official is not None and self.userIsRecipeAdmin(): | ||
82 | 897 | self.context.oci_project.setOfficialRecipeStatus( | ||
83 | 898 | self.context, official) | ||
84 | 899 | |||
85 | 861 | self.updateContextFromData(data) | 900 | self.updateContextFromData(data) |
86 | 862 | self.next_url = canonical_url(self.context) | 901 | self.next_url = canonical_url(self.context) |
87 | 863 | 902 | ||
88 | @@ -931,6 +970,11 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, | |||
89 | 931 | """See `LaunchpadFormView`.""" | 970 | """See `LaunchpadFormView`.""" |
90 | 932 | super(OCIRecipeEditView, self).setUpWidgets() | 971 | super(OCIRecipeEditView, self).setUpWidgets() |
91 | 933 | self.setUpGitRefWidget() | 972 | self.setUpGitRefWidget() |
92 | 973 | # disable the official recipe button if the user doesn't have | ||
93 | 974 | # permissions to change it | ||
94 | 975 | widget = self.widgets['official_recipe'] | ||
95 | 976 | if not self.userIsRecipeAdmin(): | ||
96 | 977 | widget.extra = "disabled='disabled'" | ||
97 | 934 | 978 | ||
98 | 935 | def setUpFields(self): | 979 | def setUpFields(self): |
99 | 936 | """See `LaunchpadFormView`.""" | 980 | """See `LaunchpadFormView`.""" |
100 | @@ -941,6 +985,16 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, | |||
101 | 941 | "The architectures that this OCI recipe builds for. Some " | 985 | "The architectures that this OCI recipe builds for. Some " |
102 | 942 | "architectures are restricted and may only be enabled or " | 986 | "architectures are restricted and may only be enabled or " |
103 | 943 | "disabled by administrators.") | 987 | "disabled by administrators.") |
104 | 988 | self.form_fields += FormFields(Bool( | ||
105 | 989 | __name__="official_recipe", | ||
106 | 990 | title="Official recipe", | ||
107 | 991 | description=( | ||
108 | 992 | "Mark this recipe as official for this OCI Project. " | ||
109 | 993 | "Allows use of distribution registry credentials " | ||
110 | 994 | "and the default git repository routing. " | ||
111 | 995 | "May only be enabled by the owner of the OCI Project."), | ||
112 | 996 | default=False, | ||
113 | 997 | required=False, readonly=False)) | ||
114 | 944 | 998 | ||
115 | 945 | def validate(self, data): | 999 | def validate(self, data): |
116 | 946 | """See `LaunchpadFormView`.""" | 1000 | """See `LaunchpadFormView`.""" |
117 | @@ -976,6 +1030,14 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, | |||
118 | 976 | # enabled. Leave it untouched. | 1030 | # enabled. Leave it untouched. |
119 | 977 | data["processors"].append(processor) | 1031 | data["processors"].append(processor) |
120 | 978 | self.validateBuildArgs(data) | 1032 | self.validateBuildArgs(data) |
121 | 1033 | official = data.get('official_recipe') | ||
122 | 1034 | official_change = self.context.official != official | ||
123 | 1035 | is_admin = self.userIsRecipeAdmin() | ||
124 | 1036 | if official is not None and official_change and not is_admin: | ||
125 | 1037 | self.setFieldError( | ||
126 | 1038 | "official_recipe", | ||
127 | 1039 | "You do not have permission to change the official status " | ||
128 | 1040 | "of this recipe.") | ||
129 | 979 | 1041 | ||
130 | 980 | 1042 | ||
131 | 981 | class OCIRecipeDeleteView(BaseOCIRecipeEditView): | 1043 | class OCIRecipeDeleteView(BaseOCIRecipeEditView): |
132 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
133 | index 3505fd3..35b9dd8 100644 | |||
134 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
135 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
136 | @@ -223,6 +223,9 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView): | |||
137 | 223 | self.assertThat( | 223 | self.assertThat( |
138 | 224 | "Build schedule:\nBuilt on request\nEdit OCI recipe\n", | 224 | "Build schedule:\nBuilt on request\nEdit OCI recipe\n", |
139 | 225 | MatchesTagText(content, "build-schedule")) | 225 | MatchesTagText(content, "build-schedule")) |
140 | 226 | self.assertThat( | ||
141 | 227 | "Official recipe:\nNo", | ||
142 | 228 | MatchesTagText(content, "official-recipe")) | ||
143 | 226 | 229 | ||
144 | 227 | def test_create_new_recipe_with_build_args(self): | 230 | def test_create_new_recipe_with_build_args(self): |
145 | 228 | oci_project = self.factory.makeOCIProject() | 231 | oci_project = self.factory.makeOCIProject() |
146 | @@ -335,6 +338,114 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView): | |||
147 | 335 | "id": "field.git_ref.repository", | 338 | "id": "field.git_ref.repository", |
148 | 336 | "value": default_repo_path}))) | 339 | "value": default_repo_path}))) |
149 | 337 | 340 | ||
150 | 341 | def test_official_is_disabled(self): | ||
151 | 342 | oci_project = self.factory.makeOCIProject() | ||
152 | 343 | browser = self.getViewBrowser( | ||
153 | 344 | oci_project, view_name="+new-recipe", user=self.person) | ||
154 | 345 | official_control = browser.getControl("Official recipe") | ||
155 | 346 | self.assertTrue(official_control.disabled) | ||
156 | 347 | |||
157 | 348 | def test_official_is_enabled(self): | ||
158 | 349 | distribution = self.factory.makeDistribution( | ||
159 | 350 | oci_project_admin=self.person) | ||
160 | 351 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
161 | 352 | browser = self.getViewBrowser( | ||
162 | 353 | oci_project, view_name="+new-recipe", user=self.person) | ||
163 | 354 | official_control = browser.getControl("Official recipe") | ||
164 | 355 | self.assertFalse(official_control.disabled) | ||
165 | 356 | |||
166 | 357 | def test_set_official(self): | ||
167 | 358 | distribution = self.factory.makeDistribution( | ||
168 | 359 | oci_project_admin=self.person) | ||
169 | 360 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
170 | 361 | [git_ref] = self.factory.makeGitRefs() | ||
171 | 362 | browser = self.getViewBrowser( | ||
172 | 363 | oci_project, view_name="+new-recipe", user=self.person) | ||
173 | 364 | browser.getControl(name="field.name").value = "recipe-name" | ||
174 | 365 | browser.getControl("Description").value = "Recipe description" | ||
175 | 366 | browser.getControl(name="field.git_ref.repository").value = ( | ||
176 | 367 | git_ref.repository.identity) | ||
177 | 368 | browser.getControl(name="field.git_ref.path").value = git_ref.path | ||
178 | 369 | official_control = browser.getControl("Official recipe") | ||
179 | 370 | official_control.selected = True | ||
180 | 371 | browser.getControl("Create OCI recipe").click() | ||
181 | 372 | |||
182 | 373 | content = find_main_content(browser.contents) | ||
183 | 374 | self.assertThat( | ||
184 | 375 | "Official recipe:\nYes", | ||
185 | 376 | MatchesTagText(content, "official-recipe")) | ||
186 | 377 | |||
187 | 378 | def test_set_official_multiple(self): | ||
188 | 379 | distribution = self.factory.makeDistribution( | ||
189 | 380 | oci_project_admin=self.person) | ||
190 | 381 | |||
191 | 382 | # do it once | ||
192 | 383 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
193 | 384 | [git_ref] = self.factory.makeGitRefs() | ||
194 | 385 | |||
195 | 386 | # and then do it again | ||
196 | 387 | oci_project2 = self.factory.makeOCIProject(pillar=distribution) | ||
197 | 388 | [git_ref2] = self.factory.makeGitRefs() | ||
198 | 389 | browser = self.getViewBrowser( | ||
199 | 390 | oci_project, view_name="+new-recipe", user=self.person) | ||
200 | 391 | browser.getControl(name="field.name").value = "recipe-name" | ||
201 | 392 | browser.getControl("Description").value = "Recipe description" | ||
202 | 393 | browser.getControl(name="field.git_ref.repository").value = ( | ||
203 | 394 | git_ref.repository.identity) | ||
204 | 395 | browser.getControl(name="field.git_ref.path").value = git_ref.path | ||
205 | 396 | official_control = browser.getControl("Official recipe") | ||
206 | 397 | official_control.selected = True | ||
207 | 398 | browser.getControl("Create OCI recipe").click() | ||
208 | 399 | |||
209 | 400 | content = find_main_content(browser.contents) | ||
210 | 401 | self.assertThat( | ||
211 | 402 | "Official recipe:\nYes", | ||
212 | 403 | MatchesTagText(content, "official-recipe")) | ||
213 | 404 | |||
214 | 405 | browser2 = self.getViewBrowser( | ||
215 | 406 | oci_project2, view_name="+new-recipe", user=self.person) | ||
216 | 407 | browser2.getControl(name="field.name").value = "recipe-name" | ||
217 | 408 | browser2.getControl("Description").value = "Recipe description" | ||
218 | 409 | browser2.getControl(name="field.git_ref.repository").value = ( | ||
219 | 410 | git_ref2.repository.identity) | ||
220 | 411 | browser2.getControl(name="field.git_ref.path").value = git_ref2.path | ||
221 | 412 | official_control = browser2.getControl("Official recipe") | ||
222 | 413 | official_control.selected = True | ||
223 | 414 | browser2.getControl("Create OCI recipe").click() | ||
224 | 415 | |||
225 | 416 | content = find_main_content(browser2.contents) | ||
226 | 417 | self.assertThat( | ||
227 | 418 | "Official recipe:\nYes", | ||
228 | 419 | MatchesTagText(content, "official-recipe")) | ||
229 | 420 | |||
230 | 421 | browser.reload() | ||
231 | 422 | content = find_main_content(browser.contents) | ||
232 | 423 | self.assertThat( | ||
233 | 424 | "Official recipe:\nYes", | ||
234 | 425 | MatchesTagText(content, "official-recipe")) | ||
235 | 426 | |||
236 | 427 | def test_set_official_no_permissions(self): | ||
237 | 428 | distro_owner = self.factory.makePerson() | ||
238 | 429 | distribution = self.factory.makeDistribution( | ||
239 | 430 | oci_project_admin=distro_owner) | ||
240 | 431 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
241 | 432 | [git_ref] = self.factory.makeGitRefs() | ||
242 | 433 | browser = self.getViewBrowser( | ||
243 | 434 | oci_project, view_name="+new-recipe", user=self.person) | ||
244 | 435 | browser.getControl(name="field.name").value = "recipe-name" | ||
245 | 436 | browser.getControl("Description").value = "Recipe description" | ||
246 | 437 | browser.getControl(name="field.git_ref.repository").value = ( | ||
247 | 438 | git_ref.repository.identity) | ||
248 | 439 | browser.getControl(name="field.git_ref.path").value = git_ref.path | ||
249 | 440 | official_control = browser.getControl("Official recipe") | ||
250 | 441 | official_control.selected = True | ||
251 | 442 | browser.getControl("Create OCI recipe").click() | ||
252 | 443 | |||
253 | 444 | error_message = ( | ||
254 | 445 | "You do not have permission to set the official status " | ||
255 | 446 | "of this recipe.") | ||
256 | 447 | self.assertIn(error_message, browser.contents) | ||
257 | 448 | |||
258 | 338 | 449 | ||
259 | 339 | class TestOCIRecipeAdminView(BaseTestOCIRecipeView): | 450 | class TestOCIRecipeAdminView(BaseTestOCIRecipeView): |
260 | 340 | 451 | ||
261 | @@ -751,6 +862,69 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView): | |||
262 | 751 | self.assertNotIn(wrong_namespace_msg, browser.contents) | 862 | self.assertNotIn(wrong_namespace_msg, browser.contents) |
263 | 752 | self.assertIn(wrong_ref_path_msg, browser.contents) | 863 | self.assertIn(wrong_ref_path_msg, browser.contents) |
264 | 753 | 864 | ||
265 | 865 | def test_official_is_disabled(self): | ||
266 | 866 | oci_project = self.factory.makeOCIProject() | ||
267 | 867 | recipe = self.factory.makeOCIRecipe( | ||
268 | 868 | registrant=self.person, owner=self.person, | ||
269 | 869 | oci_project=oci_project) | ||
270 | 870 | |||
271 | 871 | browser = self.getViewBrowser(recipe, user=self.person) | ||
272 | 872 | browser.getLink("Edit OCI recipe").click() | ||
273 | 873 | official_control = browser.getControl("Official recipe") | ||
274 | 874 | self.assertTrue(official_control.disabled) | ||
275 | 875 | |||
276 | 876 | def test_official_is_enabled(self): | ||
277 | 877 | distribution = self.factory.makeDistribution( | ||
278 | 878 | oci_project_admin=self.person) | ||
279 | 879 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
280 | 880 | recipe = self.factory.makeOCIRecipe( | ||
281 | 881 | registrant=self.person, owner=self.person, | ||
282 | 882 | oci_project=oci_project) | ||
283 | 883 | |||
284 | 884 | browser = self.getViewBrowser(recipe, user=self.person) | ||
285 | 885 | browser.getLink("Edit OCI recipe").click() | ||
286 | 886 | official_control = browser.getControl("Official recipe") | ||
287 | 887 | self.assertFalse(official_control.disabled) | ||
288 | 888 | |||
289 | 889 | def test_set_official(self): | ||
290 | 890 | distribution = self.factory.makeDistribution( | ||
291 | 891 | oci_project_admin=self.person) | ||
292 | 892 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
293 | 893 | recipe = self.factory.makeOCIRecipe( | ||
294 | 894 | registrant=self.person, owner=self.person, | ||
295 | 895 | oci_project=oci_project) | ||
296 | 896 | |||
297 | 897 | browser = self.getViewBrowser(recipe, user=self.person) | ||
298 | 898 | browser.getLink("Edit OCI recipe").click() | ||
299 | 899 | official_control = browser.getControl("Official recipe") | ||
300 | 900 | official_control.selected = True | ||
301 | 901 | browser.getControl("Update OCI recipe").click() | ||
302 | 902 | |||
303 | 903 | content = find_main_content(browser.contents) | ||
304 | 904 | self.assertThat( | ||
305 | 905 | "Official recipe:\nYes", | ||
306 | 906 | MatchesTagText(content, "official-recipe")) | ||
307 | 907 | |||
308 | 908 | def test_set_official_no_permissions(self): | ||
309 | 909 | distro_owner = self.factory.makePerson() | ||
310 | 910 | distribution = self.factory.makeDistribution( | ||
311 | 911 | oci_project_admin=distro_owner) | ||
312 | 912 | oci_project = self.factory.makeOCIProject(pillar=distribution) | ||
313 | 913 | recipe = self.factory.makeOCIRecipe( | ||
314 | 914 | registrant=self.person, owner=self.person, | ||
315 | 915 | oci_project=oci_project) | ||
316 | 916 | |||
317 | 917 | browser = self.getViewBrowser(recipe, user=self.person) | ||
318 | 918 | browser.getLink("Edit OCI recipe").click() | ||
319 | 919 | official_control = browser.getControl("Official recipe") | ||
320 | 920 | official_control.selected = True | ||
321 | 921 | browser.getControl("Update OCI recipe").click() | ||
322 | 922 | |||
323 | 923 | error_message = ( | ||
324 | 924 | "You do not have permission to change the official status " | ||
325 | 925 | "of this recipe.") | ||
326 | 926 | self.assertIn(error_message, browser.contents) | ||
327 | 927 | |||
328 | 754 | 928 | ||
329 | 755 | class TestOCIRecipeDeleteView(BaseTestOCIRecipeView): | 929 | class TestOCIRecipeDeleteView(BaseTestOCIRecipeView): |
330 | 756 | 930 | ||
331 | @@ -901,6 +1075,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): | |||
332 | 901 | Build file path: Dockerfile | 1075 | Build file path: Dockerfile |
333 | 902 | Build context directory: %s | 1076 | Build context directory: %s |
334 | 903 | Build schedule: Built on request | 1077 | Build schedule: Built on request |
335 | 1078 | Official recipe: | ||
336 | 1079 | No | ||
337 | 904 | Latest builds | 1080 | Latest builds |
338 | 905 | Status When complete Architecture | 1081 | Status When complete Architecture |
339 | 906 | Successfully built 30 minutes ago 386 | 1082 | Successfully built 30 minutes ago 386 |
340 | @@ -934,6 +1110,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): | |||
341 | 934 | Build context directory: %s | 1110 | Build context directory: %s |
342 | 935 | Build schedule: Built on request | 1111 | Build schedule: Built on request |
343 | 936 | Build-time\nARG variables: VAR1=123 VAR2=XXX | 1112 | Build-time\nARG variables: VAR1=123 VAR2=XXX |
344 | 1113 | Official recipe: | ||
345 | 1114 | No | ||
346 | 937 | Latest builds | 1115 | Latest builds |
347 | 938 | Status When complete Architecture | 1116 | Status When complete Architecture |
348 | 939 | Successfully built 30 minutes ago 386 | 1117 | Successfully built 30 minutes ago 386 |
349 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt | |||
350 | index 9a9aefc..a3e67bb 100644 | |||
351 | --- a/lib/lp/oci/templates/ocirecipe-index.pt | |||
352 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt | |||
353 | @@ -81,6 +81,13 @@ | |||
354 | 81 | <pre tal:content="build_args" /> | 81 | <pre tal:content="build_args" /> |
355 | 82 | </dd> | 82 | </dd> |
356 | 83 | </dl> | 83 | </dl> |
357 | 84 | <dl id="official-recipe"> | ||
358 | 85 | <dt>Official recipe:</dt> | ||
359 | 86 | <dd> | ||
360 | 87 | <span tal:condition="context/official">Yes</span> | ||
361 | 88 | <span tal:condition="not: context/official">No</span> | ||
362 | 89 | </dd> | ||
363 | 90 | </dl> | ||
364 | 84 | </div> | 91 | </div> |
365 | 85 | 92 | ||
366 | 86 | <h2>Latest builds</h2> | 93 | <h2>Latest builds</h2> |
367 | diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt | |||
368 | index 1cae71e..00ed4e2 100644 | |||
369 | --- a/lib/lp/oci/templates/ocirecipe-new.pt | |||
370 | +++ b/lib/lp/oci/templates/ocirecipe-new.pt | |||
371 | @@ -41,6 +41,9 @@ | |||
372 | 41 | <tal:widget define="widget nocall:view/widgets/processors"> | 41 | <tal:widget define="widget nocall:view/widgets/processors"> |
373 | 42 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | 42 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
374 | 43 | </tal:widget> | 43 | </tal:widget> |
375 | 44 | <tal:widget define="widget nocall:view/widgets/official_recipe"> | ||
376 | 45 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
377 | 46 | </tal:widget> | ||
378 | 44 | </table> | 47 | </table> |
379 | 45 | </metal:formbody> | 48 | </metal:formbody> |
380 | 46 | </div> | 49 | </div> |
381 | diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py | |||
382 | index 59bd465..1cb69af 100644 | |||
383 | --- a/lib/lp/oci/tests/test_ocirecipe.py | |||
384 | +++ b/lib/lp/oci/tests/test_ocirecipe.py | |||
385 | @@ -592,27 +592,27 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): | |||
386 | 592 | oci_project=oci_project2, registrant=owner, owner=owner) | 592 | oci_project=oci_project2, registrant=owner, owner=owner) |
387 | 593 | for _ in range(2)] | 593 | for _ in range(2)] |
388 | 594 | 594 | ||
391 | 595 | self.assertIsNone(oci_project1.getOfficialRecipe()) | 595 | self.assertTrue(oci_project1.getOfficialRecipes().is_empty()) |
392 | 596 | self.assertIsNone(oci_project2.getOfficialRecipe()) | 596 | self.assertTrue(oci_project2.getOfficialRecipes().is_empty()) |
393 | 597 | for recipe in oci_proj1_recipes + oci_proj2_recipes: | 597 | for recipe in oci_proj1_recipes + oci_proj2_recipes: |
394 | 598 | self.assertFalse(recipe.official) | 598 | self.assertFalse(recipe.official) |
395 | 599 | 599 | ||
396 | 600 | # Set official for project1 and make sure nothing else got changed. | 600 | # Set official for project1 and make sure nothing else got changed. |
397 | 601 | with StormStatementRecorder() as recorder: | 601 | with StormStatementRecorder() as recorder: |
400 | 602 | oci_project1.setOfficialRecipe(oci_proj1_recipes[0]) | 602 | oci_project1.setOfficialRecipeStatus(oci_proj1_recipes[0], True) |
401 | 603 | self.assertEqual(2, recorder.count) | 603 | self.assertEqual(1, recorder.count) |
402 | 604 | 604 | ||
404 | 605 | self.assertIsNone(oci_project2.getOfficialRecipe()) | 605 | self.assertTrue(oci_project2.getOfficialRecipes().is_empty()) |
405 | 606 | self.assertEqual( | 606 | self.assertEqual( |
407 | 607 | oci_proj1_recipes[0], oci_project1.getOfficialRecipe()) | 607 | oci_proj1_recipes[0], oci_project1.getOfficialRecipes()[0]) |
408 | 608 | self.assertTrue(oci_proj1_recipes[0].official) | 608 | self.assertTrue(oci_proj1_recipes[0].official) |
409 | 609 | for recipe in oci_proj1_recipes[1:] + oci_proj2_recipes: | 609 | for recipe in oci_proj1_recipes[1:] + oci_proj2_recipes: |
410 | 610 | self.assertFalse(recipe.official) | 610 | self.assertFalse(recipe.official) |
411 | 611 | 611 | ||
412 | 612 | # Set back no recipe as official. | 612 | # Set back no recipe as official. |
413 | 613 | with StormStatementRecorder() as recorder: | 613 | with StormStatementRecorder() as recorder: |
416 | 614 | oci_project1.setOfficialRecipe(None) | 614 | oci_project1.setOfficialRecipeStatus(oci_proj1_recipes[0], False) |
417 | 615 | self.assertEqual(1, recorder.count) | 615 | self.assertEqual(0, recorder.count) |
418 | 616 | 616 | ||
419 | 617 | for recipe in oci_proj1_recipes + oci_proj2_recipes: | 617 | for recipe in oci_proj1_recipes + oci_proj2_recipes: |
420 | 618 | self.assertFalse(recipe.official) | 618 | self.assertFalse(recipe.official) |
421 | @@ -630,7 +630,8 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): | |||
422 | 630 | oci_project=oci_project, registrant=owner) | 630 | oci_project=oci_project, registrant=owner) |
423 | 631 | 631 | ||
424 | 632 | self.assertRaises( | 632 | self.assertRaises( |
426 | 633 | ValueError, another_oci_project.setOfficialRecipe, recipe) | 633 | ValueError, another_oci_project.setOfficialRecipeStatus, |
427 | 634 | recipe, True) | ||
428 | 634 | 635 | ||
429 | 635 | def test_permission_check_on_setOfficialRecipe(self): | 636 | def test_permission_check_on_setOfficialRecipe(self): |
430 | 636 | distro = self.factory.makeDistribution() | 637 | distro = self.factory.makeDistribution() |
431 | @@ -642,7 +643,7 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory): | |||
432 | 642 | another_user = self.factory.makePerson() | 643 | another_user = self.factory.makePerson() |
433 | 643 | with person_logged_in(another_user): | 644 | with person_logged_in(another_user): |
434 | 644 | self.assertRaises( | 645 | self.assertRaises( |
436 | 645 | Unauthorized, getattr, oci_project, 'setOfficialRecipe') | 646 | Unauthorized, getattr, oci_project, 'setOfficialRecipeStatus') |
437 | 646 | 647 | ||
438 | 647 | def test_oci_project_get_recipe_by_name_and_owner(self): | 648 | def test_oci_project_get_recipe_by_name_and_owner(self): |
439 | 648 | owner = self.factory.makePerson() | 649 | owner = self.factory.makePerson() |
440 | diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py | |||
441 | index a10d9a2..380b94a 100644 | |||
442 | --- a/lib/lp/registry/browser/ociproject.py | |||
443 | +++ b/lib/lp/registry/browser/ociproject.py | |||
444 | @@ -234,7 +234,6 @@ class OCIProjectEditView(LaunchpadEditFormView): | |||
445 | 234 | schema = IOCIProject | 234 | schema = IOCIProject |
446 | 235 | field_names = [ | 235 | field_names = [ |
447 | 236 | 'name', | 236 | 'name', |
448 | 237 | 'official_recipe', | ||
449 | 238 | ] | 237 | ] |
450 | 239 | 238 | ||
451 | 240 | def setUpFields(self): | 239 | def setUpFields(self): |
452 | @@ -247,14 +246,6 @@ class OCIProjectEditView(LaunchpadEditFormView): | |||
453 | 247 | pillar_field = self.form_fields.get(pillar_key).field | 246 | pillar_field = self.form_fields.get(pillar_key).field |
454 | 248 | pillar_field.required = True | 247 | pillar_field.required = True |
455 | 249 | 248 | ||
456 | 250 | def extendFields(self): | ||
457 | 251 | official_recipe = self.context.getOfficialRecipe() | ||
458 | 252 | self.form_fields += form.Fields( | ||
459 | 253 | Choice( | ||
460 | 254 | __name__="official_recipe", title=u"Official recipe", | ||
461 | 255 | required=False, vocabulary="OCIRecipe", | ||
462 | 256 | default=official_recipe)) | ||
463 | 257 | |||
464 | 258 | @property | 249 | @property |
465 | 259 | def label(self): | 250 | def label(self): |
466 | 260 | return 'Edit %s OCI project' % self.context.name | 251 | return 'Edit %s OCI project' % self.context.name |
467 | @@ -277,9 +268,7 @@ class OCIProjectEditView(LaunchpadEditFormView): | |||
468 | 277 | 268 | ||
469 | 278 | @action('Update OCI project', name='update') | 269 | @action('Update OCI project', name='update') |
470 | 279 | def update_action(self, action, data): | 270 | def update_action(self, action, data): |
471 | 280 | official_recipe = data.pop("official_recipe") | ||
472 | 281 | self.updateContextFromData(data) | 271 | self.updateContextFromData(data) |
473 | 282 | self.context.setOfficialRecipe(official_recipe) | ||
474 | 283 | 272 | ||
475 | 284 | @property | 273 | @property |
476 | 285 | def next_url(self): | 274 | def next_url(self): |
477 | diff --git a/lib/lp/registry/browser/tests/test_ociproject.py b/lib/lp/registry/browser/tests/test_ociproject.py | |||
478 | index 3743465..a8b4113 100644 | |||
479 | --- a/lib/lp/registry/browser/tests/test_ociproject.py | |||
480 | +++ b/lib/lp/registry/browser/tests/test_ociproject.py | |||
481 | @@ -154,11 +154,9 @@ class TestOCIProjectEditView(BrowserTestCase): | |||
482 | 154 | 154 | ||
483 | 155 | layer = DatabaseFunctionalLayer | 155 | layer = DatabaseFunctionalLayer |
484 | 156 | 156 | ||
486 | 157 | def submitEditForm(self, browser, name, official_recipe=''): | 157 | def submitEditForm(self, browser, name): |
487 | 158 | browser.getLink("Edit OCI project").click() | 158 | browser.getLink("Edit OCI project").click() |
488 | 159 | browser.getControl(name="field.name").value = name | 159 | browser.getControl(name="field.name").value = name |
489 | 160 | browser.getControl(name="field.official_recipe").value = ( | ||
490 | 161 | official_recipe) | ||
491 | 162 | browser.getControl("Update OCI project").click() | 160 | browser.getControl("Update OCI project").click() |
492 | 163 | 161 | ||
493 | 164 | def test_edit_oci_project(self): | 162 | def test_edit_oci_project(self): |
494 | @@ -253,7 +251,7 @@ class TestOCIProjectEditView(BrowserTestCase): | |||
495 | 253 | view = create_initialized_view( | 251 | view = create_initialized_view( |
496 | 254 | oci_project, name="+edit", principal=oci_project.pillar.owner) | 252 | oci_project, name="+edit", principal=oci_project.pillar.owner) |
497 | 255 | view.update_action.success( | 253 | view.update_action.success( |
499 | 256 | {"name": "changed", "official_recipe": None}) | 254 | {"name": "changed"}) |
500 | 257 | self.assertSqlAttributeEqualsDate( | 255 | self.assertSqlAttributeEqualsDate( |
501 | 258 | oci_project, "date_last_modified", UTC_NOW) | 256 | oci_project, "date_last_modified", UTC_NOW) |
502 | 259 | 257 | ||
503 | @@ -280,70 +278,6 @@ class TestOCIProjectEditView(BrowserTestCase): | |||
504 | 280 | extract_text(find_tags_by_class(browser.contents, "message")[1]), | 278 | extract_text(find_tags_by_class(browser.contents, "message")[1]), |
505 | 281 | "Invalid name 'invalid name'.") | 279 | "Invalid name 'invalid name'.") |
506 | 282 | 280 | ||
507 | 283 | def test_edit_oci_project_setting_official_recipe(self): | ||
508 | 284 | self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'})) | ||
509 | 285 | |||
510 | 286 | with admin_logged_in(): | ||
511 | 287 | oci_project = self.factory.makeOCIProject() | ||
512 | 288 | user = oci_project.pillar.owner | ||
513 | 289 | recipe1 = self.factory.makeOCIRecipe( | ||
514 | 290 | registrant=user, owner=user, oci_project=oci_project) | ||
515 | 291 | recipe2 = self.factory.makeOCIRecipe( | ||
516 | 292 | registrant=user, owner=user, oci_project=oci_project) | ||
517 | 293 | |||
518 | 294 | name_value = oci_project.name | ||
519 | 295 | recipe_value = "%s/%s" % (user.name, recipe1.name) | ||
520 | 296 | |||
521 | 297 | browser = self.getViewBrowser(oci_project, user=user) | ||
522 | 298 | self.submitEditForm(browser, name_value, recipe_value) | ||
523 | 299 | |||
524 | 300 | with admin_logged_in(): | ||
525 | 301 | self.assertEqual(recipe1, oci_project.getOfficialRecipe()) | ||
526 | 302 | self.assertTrue(recipe1.official) | ||
527 | 303 | self.assertFalse(recipe2.official) | ||
528 | 304 | |||
529 | 305 | def test_edit_oci_project_overriding_official_recipe(self): | ||
530 | 306 | self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'})) | ||
531 | 307 | with admin_logged_in(): | ||
532 | 308 | oci_project = self.factory.makeOCIProject() | ||
533 | 309 | user = oci_project.pillar.owner | ||
534 | 310 | recipe1 = self.factory.makeOCIRecipe( | ||
535 | 311 | registrant=user, owner=user, oci_project=oci_project) | ||
536 | 312 | recipe2 = self.factory.makeOCIRecipe( | ||
537 | 313 | registrant=user, owner=user, oci_project=oci_project) | ||
538 | 314 | |||
539 | 315 | # Sets recipe1 as the current official one | ||
540 | 316 | oci_project.setOfficialRecipe(recipe1) | ||
541 | 317 | |||
542 | 318 | # And we will try to set recipe2 as the new official. | ||
543 | 319 | name_value = oci_project.name | ||
544 | 320 | recipe_value = "%s/%s" % (user.name, recipe2.name) | ||
545 | 321 | |||
546 | 322 | browser = self.getViewBrowser(oci_project, user=user) | ||
547 | 323 | self.submitEditForm(browser, name_value, recipe_value) | ||
548 | 324 | |||
549 | 325 | with admin_logged_in(): | ||
550 | 326 | self.assertEqual(recipe2, oci_project.getOfficialRecipe()) | ||
551 | 327 | self.assertFalse(recipe1.official) | ||
552 | 328 | self.assertTrue(recipe2.official) | ||
553 | 329 | |||
554 | 330 | def test_edit_oci_project_unsetting_official_recipe(self): | ||
555 | 331 | self.useFixture(FeatureFixture({OCI_RECIPE_ALLOW_CREATE: 'on'})) | ||
556 | 332 | with admin_logged_in(): | ||
557 | 333 | oci_project = self.factory.makeOCIProject() | ||
558 | 334 | user = oci_project.pillar.owner | ||
559 | 335 | recipe = self.factory.makeOCIRecipe( | ||
560 | 336 | registrant=user, owner=user, oci_project=oci_project) | ||
561 | 337 | oci_project.setOfficialRecipe(recipe) | ||
562 | 338 | name_value = oci_project.name | ||
563 | 339 | |||
564 | 340 | browser = self.getViewBrowser(oci_project, user=user) | ||
565 | 341 | self.submitEditForm(browser, name_value, '') | ||
566 | 342 | |||
567 | 343 | with admin_logged_in(): | ||
568 | 344 | self.assertEqual(None, oci_project.getOfficialRecipe()) | ||
569 | 345 | self.assertFalse(recipe.official) | ||
570 | 346 | |||
571 | 347 | 281 | ||
572 | 348 | class TestOCIProjectAddView(BrowserTestCase): | 282 | class TestOCIProjectAddView(BrowserTestCase): |
573 | 349 | 283 | ||
574 | diff --git a/lib/lp/registry/interfaces/ociproject.py b/lib/lp/registry/interfaces/ociproject.py | |||
575 | index cf4abef..718b9d7 100644 | |||
576 | --- a/lib/lp/registry/interfaces/ociproject.py | |||
577 | +++ b/lib/lp/registry/interfaces/ociproject.py | |||
578 | @@ -95,8 +95,8 @@ class IOCIProjectView(IHasGitRepositories, Interface): | |||
579 | 95 | def searchRecipes(query): | 95 | def searchRecipes(query): |
580 | 96 | """Searches for recipes in this OCI project.""" | 96 | """Searches for recipes in this OCI project.""" |
581 | 97 | 97 | ||
584 | 98 | def getOfficialRecipe(): | 98 | def getOfficialRecipes(): |
585 | 99 | """Gets the official recipe for this OCI project.""" | 99 | """Gets the official recipes for this OCI project.""" |
586 | 100 | 100 | ||
587 | 101 | def getDefaultGitRepository(person): | 101 | def getDefaultGitRepository(person): |
588 | 102 | """Returns the default git repository for the given user under the | 102 | """Returns the default git repository for the given user under the |
589 | @@ -147,9 +147,8 @@ class IOCIProjectEdit(Interface): | |||
590 | 147 | status=SeriesStatus.DEVELOPMENT, date_created=DEFAULT): | 147 | status=SeriesStatus.DEVELOPMENT, date_created=DEFAULT): |
591 | 148 | """Creates a new `IOCIProjectSeries`.""" | 148 | """Creates a new `IOCIProjectSeries`.""" |
592 | 149 | 149 | ||
596 | 150 | def setOfficialRecipe(recipe): | 150 | def setOfficialRecipeStatus(recipe, status): |
597 | 151 | """Sets the given recipe as the official one. If recipe is None, | 151 | """Change whether an OCI Recipe is official or not for this project.""" |
595 | 152 | the current official recipe will be unset.""" | ||
598 | 153 | 152 | ||
599 | 154 | 153 | ||
600 | 155 | class IOCIProjectLegitimate(Interface): | 154 | class IOCIProjectLegitimate(Interface): |
601 | diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py | |||
602 | index 4526724..46ea7da 100644 | |||
603 | --- a/lib/lp/registry/model/ociproject.py | |||
604 | +++ b/lib/lp/registry/model/ociproject.py | |||
605 | @@ -200,12 +200,12 @@ class OCIProject(BugTargetBase, StormBase): | |||
606 | 200 | Person.name.contains_string(query)) | 200 | Person.name.contains_string(query)) |
607 | 201 | return q.order_by(Person.name, OCIRecipe.name) | 201 | return q.order_by(Person.name, OCIRecipe.name) |
608 | 202 | 202 | ||
610 | 203 | def getOfficialRecipe(self): | 203 | def getOfficialRecipes(self): |
611 | 204 | """See `IOCIProject`.""" | 204 | """See `IOCIProject`.""" |
612 | 205 | from lp.oci.model.ocirecipe import OCIRecipe | 205 | from lp.oci.model.ocirecipe import OCIRecipe |
614 | 206 | return self.getRecipes().find(OCIRecipe._official == True).one() | 206 | return self.getRecipes().find(OCIRecipe._official == True) |
615 | 207 | 207 | ||
617 | 208 | def setOfficialRecipe(self, recipe): | 208 | def setOfficialRecipeStatus(self, recipe, status): |
618 | 209 | """See `IOCIProject`.""" | 209 | """See `IOCIProject`.""" |
619 | 210 | if recipe is not None and recipe.oci_project != self: | 210 | if recipe is not None and recipe.oci_project != self: |
620 | 211 | raise ValueError( | 211 | raise ValueError( |
621 | @@ -215,12 +215,7 @@ class OCIProject(BugTargetBase, StormBase): | |||
622 | 215 | # attribute not declared on the Interface, and we need to set it | 215 | # attribute not declared on the Interface, and we need to set it |
623 | 216 | # regardless of security checks on OCIRecipe objects. | 216 | # regardless of security checks on OCIRecipe objects. |
624 | 217 | recipe = removeSecurityProxy(recipe) | 217 | recipe = removeSecurityProxy(recipe) |
631 | 218 | previous = removeSecurityProxy(self.getOfficialRecipe()) | 218 | recipe._official = status |
626 | 219 | if previous != recipe: | ||
627 | 220 | if previous is not None: | ||
628 | 221 | previous._official = False | ||
629 | 222 | if recipe is not None: | ||
630 | 223 | recipe._official = True | ||
632 | 224 | 219 | ||
633 | 225 | def getDefaultGitRepository(self, person): | 220 | def getDefaultGitRepository(self, person): |
634 | 226 | namespace = getUtility(IGitNamespaceSet).get(person, oci_project=self) | 221 | namespace = getUtility(IGitNamespaceSet).get(person, oci_project=self) |
LGTM