Skip to content

Commit ea20d1a

Browse files
committed
Add ability to change existing roles
1 parent 590f7b2 commit ea20d1a

File tree

8 files changed

+269
-5
lines changed

8 files changed

+269
-5
lines changed

tests/unit/manage/test_views.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,152 @@ def test_post_duplicate_role(self, db_request):
215215
}
216216

217217

218+
class TestChangeProjectRoles:
219+
220+
def test_change_role(self, db_request):
221+
project = ProjectFactory.create(name="foobar")
222+
user = UserFactory.create(username="testuser")
223+
role = RoleFactory.create(
224+
user=user, project=project, role_name="Owner"
225+
)
226+
new_role_name = "Maintainer"
227+
228+
db_request.method = "POST"
229+
db_request.user = pretend.stub()
230+
db_request.POST = MultiDict({
231+
"role_id": role.id,
232+
"role_name": new_role_name,
233+
})
234+
db_request.session = pretend.stub(
235+
flash=pretend.call_recorder(lambda *a, **kw: None),
236+
)
237+
db_request.route_path = pretend.call_recorder(
238+
lambda *a, **kw: "/the-redirect"
239+
)
240+
241+
result = views.change_project_role(project, db_request)
242+
243+
assert role.role_name == new_role_name
244+
assert db_request.route_path.calls == [
245+
pretend.call('manage.project.roles', name=project.name),
246+
]
247+
assert db_request.session.flash.calls == [
248+
pretend.call("Successfully changed role", queue="success"),
249+
]
250+
assert isinstance(result, HTTPSeeOther)
251+
assert result.headers["Location"] == "/the-redirect"
252+
253+
def test_change_role_invalid_role_name(self, pyramid_request):
254+
project = pretend.stub(name="foobar")
255+
256+
pyramid_request.method = "POST"
257+
pyramid_request.POST = MultiDict({
258+
"role_id": str(uuid.uuid4()),
259+
"role_name": "Invalid Role Name",
260+
})
261+
pyramid_request.route_path = pretend.call_recorder(
262+
lambda *a, **kw: "/the-redirect"
263+
)
264+
265+
result = views.change_project_role(project, pyramid_request)
266+
267+
assert pyramid_request.route_path.calls == [
268+
pretend.call('manage.project.roles', name=project.name),
269+
]
270+
assert isinstance(result, HTTPSeeOther)
271+
assert result.headers["Location"] == "/the-redirect"
272+
273+
def test_change_role_when_multiple(self, db_request):
274+
project = ProjectFactory.create(name="foobar")
275+
user = UserFactory.create(username="testuser")
276+
owner_role = RoleFactory.create(
277+
user=user, project=project, role_name="Owner"
278+
)
279+
maintainer_role = RoleFactory.create(
280+
user=user, project=project, role_name="Maintainer"
281+
)
282+
new_role_name = "Maintainer"
283+
284+
db_request.method = "POST"
285+
db_request.user = pretend.stub()
286+
db_request.POST = MultiDict([
287+
("role_id", owner_role.id),
288+
("role_id", maintainer_role.id),
289+
("role_name", new_role_name),
290+
])
291+
db_request.session = pretend.stub(
292+
flash=pretend.call_recorder(lambda *a, **kw: None),
293+
)
294+
db_request.route_path = pretend.call_recorder(
295+
lambda *a, **kw: "/the-redirect"
296+
)
297+
298+
result = views.change_project_role(project, db_request)
299+
300+
assert db_request.db.query(Role).all() == [maintainer_role]
301+
assert db_request.route_path.calls == [
302+
pretend.call('manage.project.roles', name=project.name),
303+
]
304+
assert db_request.session.flash.calls == [
305+
pretend.call("Successfully changed role", queue="success"),
306+
]
307+
assert isinstance(result, HTTPSeeOther)
308+
assert result.headers["Location"] == "/the-redirect"
309+
310+
def test_change_missing_role(self, db_request):
311+
project = ProjectFactory.create(name="foobar")
312+
missing_role_id = str(uuid.uuid4())
313+
314+
db_request.method = "POST"
315+
db_request.user = pretend.stub()
316+
db_request.POST = MultiDict({
317+
"role_id": missing_role_id,
318+
"role_name": 'Owner',
319+
})
320+
db_request.session = pretend.stub(
321+
flash=pretend.call_recorder(lambda *a, **kw: None),
322+
)
323+
db_request.route_path = pretend.call_recorder(
324+
lambda *a, **kw: "/the-redirect"
325+
)
326+
327+
result = views.change_project_role(project, db_request)
328+
329+
assert db_request.session.flash.calls == [
330+
pretend.call("Could not find role", queue="error"),
331+
]
332+
assert isinstance(result, HTTPSeeOther)
333+
assert result.headers["Location"] == "/the-redirect"
334+
335+
def test_change_own_owner_role(self, db_request):
336+
project = ProjectFactory.create(name="foobar")
337+
user = UserFactory.create(username="testuser")
338+
role = RoleFactory.create(
339+
user=user, project=project, role_name="Owner"
340+
)
341+
342+
db_request.method = "POST"
343+
db_request.user = user
344+
db_request.POST = MultiDict({
345+
"role_id": role.id,
346+
"role_name": "Maintainer",
347+
})
348+
db_request.session = pretend.stub(
349+
flash=pretend.call_recorder(lambda *a, **kw: None),
350+
)
351+
db_request.route_path = pretend.call_recorder(
352+
lambda *a, **kw: "/the-redirect"
353+
)
354+
355+
result = views.change_project_role(project, db_request)
356+
357+
assert db_request.session.flash.calls == [
358+
pretend.call("Cannot remove yourself as Owner", queue="error"),
359+
]
360+
assert isinstance(result, HTTPSeeOther)
361+
assert result.headers["Location"] == "/the-redirect"
362+
363+
218364
class TestDeleteProjectRoles:
219365

220366
def test_delete_role(self, db_request):

tests/unit/test_routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ def add_policy(name, filename):
139139
traverse="/{name}",
140140
domain=warehouse,
141141
),
142+
pretend.call(
143+
"manage.project.change_role",
144+
"/project/{name}/collaboration/change/",
145+
factory="warehouse.packaging.models:ProjectFactory",
146+
traverse="/{name}",
147+
domain=warehouse,
148+
),
142149
pretend.call(
143150
"manage.project.delete_role",
144151
"/project/{name}/collaboration/delete/",

warehouse/manage/forms.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ class CreateRoleForm(RoleNameMixin, UsernameMixin, forms.Form):
5151
def __init__(self, *args, user_service, **kwargs):
5252
super().__init__(*args, **kwargs)
5353
self.user_service = user_service
54+
55+
56+
class ChangeRoleForm(RoleNameMixin, forms.Form):
57+
pass

warehouse/manage/views.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
from pyramid.httpexceptions import HTTPSeeOther
1616
from pyramid.security import Authenticated
1717
from pyramid.view import view_config
18+
from sqlalchemy.orm.exc import NoResultFound
1819

1920
from warehouse.accounts.interfaces import IUserService
2021
from warehouse.accounts.models import User
21-
from warehouse.manage.forms import CreateRoleForm
22+
from warehouse.manage.forms import CreateRoleForm, ChangeRoleForm
2223
from warehouse.packaging.models import Role
2324

2425

@@ -111,6 +112,66 @@ def manage_project_roles(project, request, _form_class=CreateRoleForm):
111112
}
112113

113114

115+
@view_config(
116+
route_name="manage.project.change_role",
117+
uses_session=True,
118+
require_methods=["POST"],
119+
permission="manage",
120+
)
121+
def change_project_role(project, request, _form_class=ChangeRoleForm):
122+
# This view was modified to handle deleting multiple roles for a single
123+
# user and should be updated when fixing GH-2745
124+
125+
form = _form_class(request.POST)
126+
127+
if form.validate():
128+
role_ids = request.POST.getall('role_id')
129+
130+
if len(role_ids) > 1:
131+
# This user has more than one role, so just delete all the ones
132+
# that aren't what we want.
133+
#
134+
# This should be removed when fixing GH-2745.
135+
(
136+
request.db.query(Role)
137+
.filter(
138+
Role.id.in_(role_ids),
139+
Role.project == project,
140+
Role.role_name != form.role_name.data
141+
)
142+
.delete(synchronize_session="fetch")
143+
)
144+
request.session.flash(
145+
'Successfully changed role', queue="success"
146+
)
147+
else:
148+
# This user only has one role, so get it and change the type.
149+
try:
150+
role = (
151+
request.db.query(Role)
152+
.filter(
153+
Role.id == request.POST.get('role_id'),
154+
Role.project == project,
155+
)
156+
.one()
157+
)
158+
if role.role_name == "Owner" and role.user == request.user:
159+
request.session.flash(
160+
"Cannot remove yourself as Owner", queue="error"
161+
)
162+
else:
163+
role.role_name = form.role_name.data
164+
request.session.flash(
165+
'Successfully changed role', queue="success"
166+
)
167+
except NoResultFound:
168+
request.session.flash("Could not find role", queue="error")
169+
170+
return HTTPSeeOther(
171+
request.route_path('manage.project.roles', name=project.name)
172+
)
173+
174+
114175
@view_config(
115176
route_name="manage.project.delete_role",
116177
uses_session=True,

warehouse/routes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ def includeme(config):
105105
traverse="/{name}",
106106
domain=warehouse,
107107
)
108+
config.add_route(
109+
"manage.project.change_role",
110+
"/project/{name}/collaboration/change/",
111+
factory="warehouse.packaging.models:ProjectFactory",
112+
traverse="/{name}",
113+
domain=warehouse,
114+
)
108115
config.add_route(
109116
"manage.project.delete_role",
110117
"/project/{name}/collaboration/delete/",

warehouse/static/js/warehouse/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,22 @@ docReady(() => {
143143
});
144144
}
145145
});
146+
147+
docReady(() => {
148+
let changeRoleForms = document.querySelectorAll("form.changeRole");
149+
150+
if (changeRoleForms) {
151+
for (let form of changeRoleForms) {
152+
let changeButton = form.querySelector("button.change-button");
153+
let changeSelect = form.querySelector("select.change-field");
154+
155+
changeSelect.addEventListener("change", function (event) {
156+
if (event.target.value === changeSelect.dataset.original) {
157+
changeButton.style.visibility = "hidden";
158+
} else {
159+
changeButton.style.visibility = "visible";
160+
}
161+
});
162+
}
163+
}
164+
});

warehouse/static/sass/blocks/_button.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,8 @@
155155
}
156156
}
157157
}
158+
159+
160+
.change-button {
161+
visibility: hidden;
162+
}

warehouse/templates/manage/roles.html

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,26 @@ <h2>Collaborators</h2>
6060
{% if role.user == request.user %}
6161
{{ role.role_name }}
6262
{% else %}
63-
<form>
64-
<select name="" id="">
65-
<option value="">Owner</option>
66-
<option value="">Maintainer</option>
63+
<form class="changeRole" method="POST" action="{{ request.route_path('manage.project.change_role', name=project.name) }}">
64+
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
65+
{% for role in roles %}
66+
<input type="hidden" name="role_id" value="{{ role.id }}">
67+
{% endfor %}
68+
69+
<select class="change-field" name="role_name" data-original="{{ role.role_name }}">
70+
{% for role_name in ['Owner', 'Maintainer'] %}
71+
<option value="{{ role_name }}" {{ 'selected' if role_name == role.role_name else '' }}>
72+
{{ role_name }}
73+
</option>
74+
{% endfor %}
6775
</select>
76+
<button
77+
class="button change-button"
78+
type="submit"
79+
title="Save this new role"
80+
>
81+
Save
82+
</button>
6883
</form>
6984
{% endif %}
7085
</td>

0 commit comments

Comments
 (0)