mirror of
https://github.com/bugsink/bugsink.git
synced 2025-12-29 09:20:23 -06:00
WIP teams & project-management (3)
This commit is contained in:
@@ -41,3 +41,6 @@ class TeamMembership(models.Model):
|
||||
|
||||
class Meta:
|
||||
unique_together = ("team", "user")
|
||||
|
||||
def is_admin(self):
|
||||
return self.role == TeamRole.ADMIN
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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 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 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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}),
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user