Browse Source

feat: add manager role

pull/551/head
swoga 3 years ago
committed by Pēteris Caune
parent
commit
9640d2242f
7 changed files with 87 additions and 25 deletions
  1. +2
    -2
      hc/accounts/forms.py
  2. +17
    -0
      hc/accounts/migrations/0043_add_role_manager.py
  3. +3
    -3
      hc/accounts/models.py
  4. +36
    -9
      hc/accounts/tests/test_project.py
  5. +10
    -3
      hc/accounts/views.py
  6. +1
    -1
      hc/test.py
  7. +18
    -7
      templates/accounts/project.html

+ 2
- 2
hc/accounts/forms.py View File

@ -6,7 +6,7 @@ from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.models import User 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 from hc.api.models import TokenBucket
import pytz import pytz
@ -136,7 +136,7 @@ class ChangeEmailForm(forms.Form):
class InviteTeamMemberForm(forms.Form): class InviteTeamMemberForm(forms.Form):
email = LowercaseEmailField(max_length=254) email = LowercaseEmailField(max_length=254)
rw = forms.BooleanField(required=False)
role = forms.ChoiceField(choices=Member.Role.choices)
class RemoveTeamMemberForm(forms.Form): class RemoveTeamMemberForm(forms.Form):


+ 17
- 0
hc/accounts/migrations/0043_add_role_manager.py View 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),
),
]

+ 3
- 3
hc/accounts/models.py View File

@ -351,14 +351,13 @@ class Project(models.Model):
used = q.distinct().count() used = q.distinct().count()
return used < self.owner_profile.team_limit 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(): if Member.objects.filter(user=user, project=self).exists():
return False return False
if self.owner_id == user.id: if self.owner_id == user.id:
return False return False
role = Member.Role.REGULAR if rw else Member.Role.READONLY
Member.objects.create(user=user, project=self, role=role) Member.objects.create(user=user, project=self, role=role)
checks_url = reverse("hc-checks", args=[self.code]) checks_url = reverse("hc-checks", args=[self.code])
user.profile.send_instant_login_link(self, redirect_url=checks_url) user.profile.send_instant_login_link(self, redirect_url=checks_url)
@ -423,6 +422,7 @@ class Member(models.Model):
class Role(models.TextChoices): class Role(models.TextChoices):
READONLY = "r", "Read-only" READONLY = "r", "Read-only"
REGULAR = "w", "Member" REGULAR = "w", "Member"
MANAGER = "m", "Manager"
user = models.ForeignKey(User, models.CASCADE, related_name="memberships") user = models.ForeignKey(User, models.CASCADE, related_name="memberships")
project = models.ForeignKey(Project, models.CASCADE) project = models.ForeignKey(Project, models.CASCADE)
@ -441,7 +441,7 @@ class Member(models.Model):
@property @property
def is_rw(self): 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): class Credential(models.Model):


+ 36
- 9
hc/accounts/tests/test_project.py View File

@ -66,7 +66,7 @@ class ProjectTestCase(BaseTestCase):
def test_it_adds_team_member(self): def test_it_adds_team_member(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]", "rw": "1"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "w"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -90,7 +90,7 @@ class ProjectTestCase(BaseTestCase):
def test_it_adds_readonly_team_member(self): def test_it_adds_readonly_team_member(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -100,6 +100,20 @@ class ProjectTestCase(BaseTestCase):
self.assertEqual(member.role, member.Role.READONLY) self.assertEqual(member.role, member.Role.READONLY)
def test_it_adds_manager_team_member(self):
self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]", "role": "m"}
r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200)
member = Member.objects.get(
project=self.project, user__email="[email protected]"
)
# The new user should have role manager
self.assertEqual(member.role, member.Role.MANAGER)
def test_it_adds_member_from_another_team(self): def test_it_adds_member_from_another_team(self):
# With team limit at zero, we should not be able to invite any new users # With team limit at zero, we should not be able to invite any new users
self.profile.team_limit = 0 self.profile.team_limit = 0
@ -111,7 +125,7 @@ class ProjectTestCase(BaseTestCase):
Member.objects.create(user=self.charlie, project=p2) Member.objects.create(user=self.charlie, project=p2)
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -125,7 +139,7 @@ class ProjectTestCase(BaseTestCase):
def test_it_rejects_duplicate_membership(self): def test_it_rejects_duplicate_membership(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "[email protected] is already a member") self.assertContains(r, "[email protected] is already a member")
@ -135,7 +149,7 @@ class ProjectTestCase(BaseTestCase):
def test_it_rejects_owner_as_a_member(self): def test_it_rejects_owner_as_a_member(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "[email protected] is already a member") self.assertContains(r, "[email protected] is already a member")
@ -146,7 +160,7 @@ class ProjectTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
aaa = "a" * 300 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) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -161,7 +175,7 @@ class ProjectTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "Too Many Requests") self.assertContains(r, "Too Many Requests")
@ -170,7 +184,7 @@ class ProjectTestCase(BaseTestCase):
def test_it_requires_owner_to_add_team_member(self): def test_it_requires_owner_to_add_team_member(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
@ -180,7 +194,7 @@ class ProjectTestCase(BaseTestCase):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
form = {"invite_team_member": "1", "email": "[email protected]"}
form = {"invite_team_member": "1", "email": "[email protected]", "role": "r"}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403) self.assertEqual(r.status_code, 403)
@ -200,6 +214,19 @@ class ProjectTestCase(BaseTestCase):
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertEqual(r.status_code, 403) 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="[email protected]", password="password")
form = {"remove_team_member": "1", "email": "[email protected]"}
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): def test_it_checks_membership_when_removing_team_member(self):
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")


+ 10
- 3
hc/accounts/views.py View File

@ -284,9 +284,11 @@ def project(request, code):
is_owner = project.owner_id == request.user.id is_owner = project.owner_id == request.user.id
if request.user.is_superuser or is_owner: if request.user.is_superuser or is_owner:
is_manager = True
rw = True rw = True
else: else:
membership = get_object_or_404(Member, project=project, user=request.user) membership = get_object_or_404(Member, project=project, user=request.user)
is_manager = membership.role == Member.Role.MANAGER
rw = membership.is_rw rw = membership.is_rw
ctx = { ctx = {
@ -294,6 +296,7 @@ def project(request, code):
"rw": rw, "rw": rw,
"project": project, "project": project,
"is_owner": is_owner, "is_owner": is_owner,
"is_manager": is_manager,
"show_api_keys": "show_api_keys" in request.GET, "show_api_keys": "show_api_keys" in request.GET,
"enable_prometheus": settings.PROMETHEUS_ENABLED is True, "enable_prometheus": settings.PROMETHEUS_ENABLED is True,
} }
@ -319,7 +322,7 @@ def project(request, code):
elif "show_api_keys" in request.POST: elif "show_api_keys" in request.POST:
ctx["show_api_keys"] = True ctx["show_api_keys"] = True
elif "invite_team_member" in request.POST: elif "invite_team_member" in request.POST:
if not is_owner:
if not is_manager:
return HttpResponseForbidden() return HttpResponseForbidden()
form = forms.InviteTeamMemberForm(request.POST) form = forms.InviteTeamMemberForm(request.POST)
@ -341,7 +344,7 @@ def project(request, code):
except User.DoesNotExist: except User.DoesNotExist:
user = _make_user(email, with_project=False) 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_member_invited"] = email
ctx["team_status"] = "success" ctx["team_status"] = "success"
else: else:
@ -349,7 +352,7 @@ def project(request, code):
ctx["team_status"] = "info" ctx["team_status"] = "info"
elif "remove_team_member" in request.POST: elif "remove_team_member" in request.POST:
if not is_owner:
if not is_manager:
return HttpResponseForbidden() return HttpResponseForbidden()
form = forms.RemoveTeamMemberForm(request.POST) form = forms.RemoveTeamMemberForm(request.POST)
@ -361,6 +364,9 @@ def project(request, code):
if farewell_user is None: if farewell_user is None:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if farewell_user == request.user:
return HttpResponseBadRequest()
Member.objects.filter(project=project, user=farewell_user).delete() Member.objects.filter(project=project, user=farewell_user).delete()
ctx["team_member_removed"] = form.cleaned_data["email"] ctx["team_member_removed"] = form.cleaned_data["email"]
@ -428,6 +434,7 @@ def project(request, code):
project.save() project.save()
ctx["is_owner"] = True ctx["is_owner"] = True
ctx["is_manager"] = True
messages.success(request, "You are now the owner of this project!") messages.success(request, "You are now the owner of this project!")
elif "reject_transfer" in request.POST: elif "reject_transfer" in request.POST:


+ 1
- 1
hc/test.py View File

@ -36,7 +36,7 @@ class BaseTestCase(TestCase):
self.bobs_profile.save() self.bobs_profile.save()
self.bobs_membership = Member.objects.create( 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 # Charlie should have no access to Alice's stuff


+ 18
- 7
templates/accounts/project.html View File

@ -169,7 +169,7 @@
<td class="email">{{ m.user.email }}</td> <td class="email">{{ m.user.email }}</td>
<td>{{ m.get_role_display}}</td> <td>{{ m.get_role_display}}</td>
<td> <td>
{% if is_owner %}
{% if is_manager and m.user != request.user %}
<a <a
href="#" href="#"
data-email="{{ m.user.email }}" data-email="{{ m.user.email }}"
@ -179,7 +179,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
{% if is_owner and invite_suggestions %}
{% if is_manager and invite_suggestions %}
<tr id="suggestions-row"> <tr id="suggestions-row">
<td colspan="3"> <td colspan="3">
Add Users from Other Teams Add Users from Other Teams
@ -210,7 +210,7 @@
<br /> <br />
{% if is_owner %}
{% if is_manager %}
{% if project.can_invite_new_users %} {% if project.can_invite_new_users %}
<a <a
href="#" href="#"
@ -401,8 +401,19 @@
<label class="radio-container"> <label class="radio-container">
<input <input
type="radio" 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> checked>
<span class="radiomark"></span> <span class="radiomark"></span>
Team Member Team Member
@ -410,8 +421,8 @@
<label class="radio-container"> <label class="radio-container">
<input <input
type="radio" type="radio"
name="rw"
value="">
name="role"
value="r">
<span class="radiomark"></span> <span class="radiomark"></span>
Read-only Read-only
<span class="help-block"> <span class="help-block">


Loading…
Cancel
Save