forked from GithubBackups/healthchecks
feat: add manager role
This commit is contained in:
parent
ce9ff3ac42
commit
9640d2242f
@ -6,7 +6,7 @@ from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import User
|
||||
from hc.accounts.models import REPORT_CHOICES
|
||||
from hc.accounts.models import REPORT_CHOICES, Member
|
||||
from hc.api.models import TokenBucket
|
||||
import pytz
|
||||
|
||||
@ -136,7 +136,7 @@ class ChangeEmailForm(forms.Form):
|
||||
|
||||
class InviteTeamMemberForm(forms.Form):
|
||||
email = LowercaseEmailField(max_length=254)
|
||||
rw = forms.BooleanField(required=False)
|
||||
role = forms.ChoiceField(choices=Member.Role.choices)
|
||||
|
||||
|
||||
class RemoveTeamMemberForm(forms.Form):
|
||||
|
17
hc/accounts/migrations/0043_add_role_manager.py
Normal file
17
hc/accounts/migrations/0043_add_role_manager.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
from hc.accounts.models import Member
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0042_remove_member_rw'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='member',
|
||||
name='role',
|
||||
field=models.CharField(choices=Member.Role.choices, default=Member.Role.REGULAR, max_length=1),
|
||||
),
|
||||
]
|
@ -351,14 +351,13 @@ class Project(models.Model):
|
||||
used = q.distinct().count()
|
||||
return used < self.owner_profile.team_limit
|
||||
|
||||
def invite(self, user, rw):
|
||||
def invite(self, user, role):
|
||||
if Member.objects.filter(user=user, project=self).exists():
|
||||
return False
|
||||
|
||||
if self.owner_id == user.id:
|
||||
return False
|
||||
|
||||
role = Member.Role.REGULAR if rw else Member.Role.READONLY
|
||||
Member.objects.create(user=user, project=self, role=role)
|
||||
checks_url = reverse("hc-checks", args=[self.code])
|
||||
user.profile.send_instant_login_link(self, redirect_url=checks_url)
|
||||
@ -423,6 +422,7 @@ class Member(models.Model):
|
||||
class Role(models.TextChoices):
|
||||
READONLY = "r", "Read-only"
|
||||
REGULAR = "w", "Member"
|
||||
MANAGER = "m", "Manager"
|
||||
|
||||
user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
|
||||
project = models.ForeignKey(Project, models.CASCADE)
|
||||
@ -441,7 +441,7 @@ class Member(models.Model):
|
||||
|
||||
@property
|
||||
def is_rw(self):
|
||||
return self.role in (Member.Role.REGULAR,)
|
||||
return self.role in (Member.Role.REGULAR, Member.Role.MANAGER)
|
||||
|
||||
|
||||
class Credential(models.Model):
|
||||
|
@ -66,7 +66,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
def test_it_adds_team_member(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "rw": "1"}
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "w"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@ -90,7 +90,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
def test_it_adds_readonly_team_member(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@ -100,6 +100,20 @@ class ProjectTestCase(BaseTestCase):
|
||||
|
||||
self.assertEqual(member.role, member.Role.READONLY)
|
||||
|
||||
def test_it_adds_manager_team_member(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "m"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
member = Member.objects.get(
|
||||
project=self.project, user__email="frank@example.org"
|
||||
)
|
||||
|
||||
# The new user should have role manager
|
||||
self.assertEqual(member.role, member.Role.MANAGER)
|
||||
|
||||
def test_it_adds_member_from_another_team(self):
|
||||
# With team limit at zero, we should not be able to invite any new users
|
||||
self.profile.team_limit = 0
|
||||
@ -111,7 +125,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
Member.objects.create(user=self.charlie, project=p2)
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
form = {"invite_team_member": "1", "email": "charlie@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "charlie@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@ -125,7 +139,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
def test_it_rejects_duplicate_membership(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "bob@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "bob@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertContains(r, "bob@example.org is already a member")
|
||||
|
||||
@ -135,7 +149,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
def test_it_rejects_owner_as_a_member(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "alice@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "alice@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertContains(r, "alice@example.org is already a member")
|
||||
|
||||
@ -146,7 +160,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
aaa = "a" * 300
|
||||
form = {"invite_team_member": "1", "email": f"frank+{aaa}@example.org"}
|
||||
form = {"invite_team_member": "1", "email": f"frank+{aaa}@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@ -161,7 +175,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertContains(r, "Too Many Requests")
|
||||
|
||||
@ -170,7 +184,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
def test_it_requires_owner_to_add_team_member(self):
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
@ -180,7 +194,7 @@ class ProjectTestCase(BaseTestCase):
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org"}
|
||||
form = {"invite_team_member": "1", "email": "frank@example.org", "role": "r"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
@ -200,6 +214,19 @@ class ProjectTestCase(BaseTestCase):
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
def test_it_rejects_manager_remove_self(self):
|
||||
self.bobs_membership.role = "m"
|
||||
self.bobs_membership.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
|
||||
form = {"remove_team_member": "1", "email": "bob@example.org"}
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
# The number of memberships should have not decreased
|
||||
self.assertEqual(self.project.member_set.count(), 1)
|
||||
|
||||
def test_it_checks_membership_when_removing_team_member(self):
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
|
||||
|
@ -284,9 +284,11 @@ def project(request, code):
|
||||
is_owner = project.owner_id == request.user.id
|
||||
|
||||
if request.user.is_superuser or is_owner:
|
||||
is_manager = True
|
||||
rw = True
|
||||
else:
|
||||
membership = get_object_or_404(Member, project=project, user=request.user)
|
||||
is_manager = membership.role == Member.Role.MANAGER
|
||||
rw = membership.is_rw
|
||||
|
||||
ctx = {
|
||||
@ -294,6 +296,7 @@ def project(request, code):
|
||||
"rw": rw,
|
||||
"project": project,
|
||||
"is_owner": is_owner,
|
||||
"is_manager": is_manager,
|
||||
"show_api_keys": "show_api_keys" in request.GET,
|
||||
"enable_prometheus": settings.PROMETHEUS_ENABLED is True,
|
||||
}
|
||||
@ -319,7 +322,7 @@ def project(request, code):
|
||||
elif "show_api_keys" in request.POST:
|
||||
ctx["show_api_keys"] = True
|
||||
elif "invite_team_member" in request.POST:
|
||||
if not is_owner:
|
||||
if not is_manager:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
form = forms.InviteTeamMemberForm(request.POST)
|
||||
@ -341,7 +344,7 @@ def project(request, code):
|
||||
except User.DoesNotExist:
|
||||
user = _make_user(email, with_project=False)
|
||||
|
||||
if project.invite(user, rw=form.cleaned_data["rw"]):
|
||||
if project.invite(user, role=form.cleaned_data["role"]):
|
||||
ctx["team_member_invited"] = email
|
||||
ctx["team_status"] = "success"
|
||||
else:
|
||||
@ -349,7 +352,7 @@ def project(request, code):
|
||||
ctx["team_status"] = "info"
|
||||
|
||||
elif "remove_team_member" in request.POST:
|
||||
if not is_owner:
|
||||
if not is_manager:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
form = forms.RemoveTeamMemberForm(request.POST)
|
||||
@ -361,6 +364,9 @@ def project(request, code):
|
||||
if farewell_user is None:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if farewell_user == request.user:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
Member.objects.filter(project=project, user=farewell_user).delete()
|
||||
|
||||
ctx["team_member_removed"] = form.cleaned_data["email"]
|
||||
@ -428,6 +434,7 @@ def project(request, code):
|
||||
project.save()
|
||||
|
||||
ctx["is_owner"] = True
|
||||
ctx["is_manager"] = True
|
||||
messages.success(request, "You are now the owner of this project!")
|
||||
|
||||
elif "reject_transfer" in request.POST:
|
||||
|
@ -36,7 +36,7 @@ class BaseTestCase(TestCase):
|
||||
self.bobs_profile.save()
|
||||
|
||||
self.bobs_membership = Member.objects.create(
|
||||
user=self.bob, project=self.project
|
||||
user=self.bob, project=self.project, role=Member.Role.REGULAR
|
||||
)
|
||||
|
||||
# Charlie should have no access to Alice's stuff
|
||||
|
@ -169,7 +169,7 @@
|
||||
<td class="email">{{ m.user.email }}</td>
|
||||
<td>{{ m.get_role_display}}</td>
|
||||
<td>
|
||||
{% if is_owner %}
|
||||
{% if is_manager and m.user != request.user %}
|
||||
<a
|
||||
href="#"
|
||||
data-email="{{ m.user.email }}"
|
||||
@ -179,7 +179,7 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if is_owner and invite_suggestions %}
|
||||
{% if is_manager and invite_suggestions %}
|
||||
<tr id="suggestions-row">
|
||||
<td colspan="3">
|
||||
Add Users from Other Teams
|
||||
@ -210,7 +210,7 @@
|
||||
|
||||
<br />
|
||||
|
||||
{% if is_owner %}
|
||||
{% if is_manager %}
|
||||
{% if project.can_invite_new_users %}
|
||||
<a
|
||||
href="#"
|
||||
@ -401,8 +401,19 @@
|
||||
<label class="radio-container">
|
||||
<input
|
||||
type="radio"
|
||||
name="rw"
|
||||
value="1"
|
||||
name="role"
|
||||
value="m">
|
||||
<span class="radiomark"></span>
|
||||
Manager
|
||||
<span class="help-block">
|
||||
Can invite/remove other members.
|
||||
</span>
|
||||
</label>
|
||||
<label class="radio-container">
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
value="w"
|
||||
checked>
|
||||
<span class="radiomark"></span>
|
||||
Team Member
|
||||
@ -410,8 +421,8 @@
|
||||
<label class="radio-container">
|
||||
<input
|
||||
type="radio"
|
||||
name="rw"
|
||||
value="">
|
||||
name="role"
|
||||
value="r">
|
||||
<span class="radiomark"></span>
|
||||
Read-only
|
||||
<span class="help-block">
|
||||
|
Loading…
x
Reference in New Issue
Block a user