WIP teams & project-management (3)

This commit is contained in:
Klaas van Schelven
2024-06-05 20:23:28 +02:00
parent 7dddf45a04
commit d0bfa668ba
6 changed files with 70 additions and 22 deletions

View File

@@ -41,3 +41,6 @@ class TeamMembership(models.Model):
class Meta:
unique_together = ("team", "user")
def is_admin(self):
return self.role == TeamRole.ADMIN

View File

@@ -20,7 +20,7 @@ def send_team_invite_email_new_user(email, team_pk, token):
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"team_name": team.name,
"url": reverse("team_members_accept_new_user", kwargs={
"url": get_settings().BASE_URL + reverse("team_members_accept_new_user", kwargs={
"token": token,
"team_pk": team_pk,
}),
@@ -40,7 +40,7 @@ def send_team_invite_email(email, team_pk):
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"team_name": team.name,
"url": reverse("team_members_accept", kwargs={
"url": get_settings().BASE_URL + reverse("team_members_accept", kwargs={
"team_pk": team_pk,
}),
},

View File

@@ -28,37 +28,43 @@
<table class="w-full">
<tbody>
{% for member in member_list %}
{% for team in team_list %}
<tr class="bg-white border-slate-200 border-b-2">
<td class="w-full p-4">
<div class="text-xl font-bold text-cyan-500">
<a href={% url "team_member_settings" team_pk=member.team.id user_pk=request.user.id %}>{{ member.team.name }}</a>
<a href={% url "team_member_settings" team_pk=team.id user_pk=request.user.id %}>{{ team.name }}</a>
</div>
<div>
{{ member.project_count }} projects | {{ member.member_count }} members
{{ team.project_count }} projects | {{ team.member_count }} members
</div>
</td>
<td class="pr-2 text-center">
{% if not member.accepted %}
<span class="bg-slate-100 rounded-2xl px-4 py-2 ml-2 text-sm">Invitation&nbsp;pending</span>
{% elif member.role == 1 %} {# NOTE: we intentionally hide admin-ness for non-accepted users; TODO better use of constants #}
<span class="bg-cyan-100 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
{% if team.member %}
{% if not team.member.accepted %}
<span class="bg-slate-100 rounded-2xl px-4 py-2 ml-2 text-sm">You're&nbsp;invited!</span>
{% elif team.member.role == 1 %} {# NOTE: we intentionally hide admin-ness for non-accepted users; TODO better use of constants #}
<span class="bg-cyan-100 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
{% endif %}
{% endif %}
</td>
<td class="pr-2">
{% if team.member.is_admin %}
<div class="rounded-full hover:bg-slate-100 p-2 cursor-pointer" onclick="followContainedLink(this);" >
<a href="{% url 'team_members' team_pk=member.team.id %}">
<a href="{% url 'team_members' team_pk=team.id %}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
</svg>
</a>
</div>
{% endif %}
</td>
<td class="pr-2">
{% if team.member.is_admin %}
<div class="rounded-full hover:bg-slate-100 p-2 cursor-pointer"onclick="followContainedLink(this);" >
<a href="TODO">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-8">
@@ -67,16 +73,33 @@
</svg>
</a>
</div>
{% endif %}
</td>
<td>
<div>
<button name="action" value="leave:{{ member.team.id }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 active:ring rounded-md">Leave</button>
</div>
{% if team.member %}
{% if not team.member.accepted %}
<div>
<a href="{% url 'team_members_accept' team_pk=team.id %}" class="font-bold text-cyan-500">Invitation</a>
</div>
{% elif team.member.role == 1 %} {# NOTE: we intentionally hide admin-ness for non-accepted users; TODO better use of constants #}
<div>
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 border-slate-300 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 active:ring rounded-md">Leave</button>
</div>
{% endif %}
{% endif %}
</td>
</tr>
{% empty %}
<tr class="bg-white border-slate-200 border-b-2">
<td class="w-full p-4">
No teams found.
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -63,6 +63,16 @@
</td>
</tr>
{% empty %}
<tr class="bg-white border-slate-200 border-b-2">
<td class="w-full p-4">
<div>
{# Note: this is already somewhat exceptional, because the usually you'll at least see yourself here (unless you're a superuser and a team has become memberless) #}
No members yet. <a href="{% url "team_members_invite" team_pk=team.pk %}" class="text-cyan-500 font-bold">Invite someone</a>.
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -28,22 +28,34 @@ def team_list(request, ownership_filter="mine"):
TeamMembership.objects.filter(team=team_pk, user=request.user.id).delete()
# messages.success("User removed from team") I think this will be obvious enough
my_memberships = TeamMembership.objects.filter(user=request.user)
if ownership_filter == "mine":
base_qs = TeamMembership.objects.filter(user=request.user)
base_qs = Team.objects.filter(teammembership__in=my_memberships)
elif ownership_filter == "other":
base_qs = TeamMembership.objects.exclude(user=request.user).distinct("team") # TODO filter on minimal visibility
base_qs = Team.objects.exclude(teammembership__in=my_memberships).distinct()
else:
raise ValueError("Invalid ownership_filter")
# select member_list with associated counts (active i.e. accepted members)
member_list = base_qs.select_related('team').annotate(
project_count=models.Count('team__project', distinct=True),
member_count=models.Count('team__teammembership', distinct=True, filter=models.Q(team__teammembership__accepted=True)),
team_list = base_qs.annotate(
project_count=models.Count('project', distinct=True),
member_count=models.Count('teammembership', distinct=True, filter=models.Q(teammembership__accepted=True)),
)
if ownership_filter == "mine":
# Perhaps there's some Django-native way of doing this, but I can't figure it out soon enough, and this also
# works:
my_memberships_dict = {m.team_id: m for m in my_memberships}
team_list_2 = []
for team in team_list:
team.member = my_memberships_dict.get(team.id)
team_list_2.append(team)
team_list = team_list_2
return render(request, 'teams/team_list.html', {
'ownership_filter': ownership_filter,
'member_list': member_list,
'team_list': team_list,
})

View File

@@ -15,7 +15,7 @@ def send_confirm_email(email, token):
context={
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"confirm_url": reverse("confirm_email", kwargs={"token": token}),
"confirm_url": get_settings().BASE_URL + reverse("confirm_email", kwargs={"token": token}),
},
)
@@ -29,6 +29,6 @@ def send_reset_email(email, token):
context={
"site_title": get_settings().SITE_TITLE,
"base_url": get_settings().BASE_URL + "/",
"reset_url": reverse("reset_email", kwargs={"token": token}),
"reset_url": get_settings().BASE_URL + reverse("reset_password", kwargs={"token": token}),
},
)