From a631e1ba4ded104aa3746c786bbe0bd7937d58e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 14:29:46 +0200 Subject: [PATCH 01/31] UI: Change template draft_finalise intro text display Part of #106 --- extensions/templates/extensions/draft_finalise.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/templates/extensions/draft_finalise.html b/extensions/templates/extensions/draft_finalise.html index 1ff3adfd..04f8b3b2 100644 --- a/extensions/templates/extensions/draft_finalise.html +++ b/extensions/templates/extensions/draft_finalise.html @@ -25,7 +25,7 @@ {% csrf_token %} {% with form=form|add_form_classes extension_form=extension_form|add_form_classes %} -
+

{% blocktranslate with type=type|lower %} Please check and edit your {{ type }}'s details before submitting it for review. -- 2.30.2 From ee555a7eca0f9658d90ff8b2eac9557106a959f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 15:40:25 +0200 Subject: [PATCH 02/31] Teams: Add template draft_finalize select team markup front-end Part of #106 --- .../templates/extensions/draft_finalise.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/extensions/templates/extensions/draft_finalise.html b/extensions/templates/extensions/draft_finalise.html index 04f8b3b2..a4e4f831 100644 --- a/extensions/templates/extensions/draft_finalise.html +++ b/extensions/templates/extensions/draft_finalise.html @@ -40,6 +40,26 @@

+ {% if user.team_users %} +
+
+ {# TODO: @back-end process Select Team with Django #} + * + +
+
+
+ {% endif %} + {% for field in extension_form %} {% if field != 'tags' %} {# TODO: fix handling of tags #} -- 2.30.2 From f63aea3675843598814319bf799041e3a3ba895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 15:40:50 +0200 Subject: [PATCH 03/31] Teams: Add template update select team markup front-end Part of #106 --- .../templates/extensions/manage/update.html | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/extensions/templates/extensions/manage/update.html b/extensions/templates/extensions/manage/update.html index 05edd9ad..39160a33 100644 --- a/extensions/templates/extensions/manage/update.html +++ b/extensions/templates/extensions/manage/update.html @@ -29,6 +29,26 @@ {{ form.errors }}
+ {% if user.team_users %} +
+
+ {# TODO: @back-end process Assign Team with Django #} + * + +
+
+
+ {% endif %} +
{% include "common/components/field.html" with field=form.description label="Description" placeholder="Describe the extension..." %}
-- 2.30.2 From 41f1e98367f200268eadab2bb28620a9a85c0199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 15:53:11 +0200 Subject: [PATCH 04/31] UI: Add template team_list btn leave team markup base Part of #106 --- teams/templates/teams/team_list.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/teams/templates/teams/team_list.html b/teams/templates/teams/team_list.html index 5c80005b..e907d4a7 100644 --- a/teams/templates/teams/team_list.html +++ b/teams/templates/teams/team_list.html @@ -13,6 +13,7 @@ Role + @@ -27,6 +28,20 @@ {{ team_member.get_role_display }} + + {# TODO: disable dropdown btn leave team if user is the only manager #} + + {% endwith %} {% endfor %} -- 2.30.2 From becc30e26aafa09c52d7a4ec8e53890e6671aa8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 16:06:27 +0200 Subject: [PATCH 05/31] UI: Add template team_list team users count display Part of #106 --- teams/templates/teams/team_list.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/teams/templates/teams/team_list.html b/teams/templates/teams/team_list.html index e907d4a7..179daee6 100644 --- a/teams/templates/teams/team_list.html +++ b/teams/templates/teams/team_list.html @@ -13,6 +13,9 @@ Role + + Users + @@ -28,6 +31,11 @@ {{ team_member.get_role_display }} + +
+ {{ team.team_users.all.count }} +
+ {# TODO: disable dropdown btn leave team if user is the only manager #} -- 2.30.2 From 9c1f011b3cf8970739fc3de051a0a26787357059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 16:48:12 +0200 Subject: [PATCH 07/31] Fix: Style nav-pills-divider spacing --- common/static/common/styles/_extension.sass | 1 + 1 file changed, 1 insertion(+) diff --git a/common/static/common/styles/_extension.sass b/common/static/common/styles/_extension.sass index 5e9162cc..f16451d8 100644 --- a/common/static/common/styles/_extension.sass +++ b/common/static/common/styles/_extension.sass @@ -367,6 +367,7 @@ @extend .dropdown-divider +margin(0, top) + +margin(1, bottom) a &.dropdown-item -- 2.30.2 From e5b6a0d4550590cf0b5f51be1abfe80ffdbff3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Wed, 8 May 2024 17:03:11 +0200 Subject: [PATCH 08/31] Teams: Enable template tabs nav item teams for all users Part of #106 --- users/templates/users/settings/tabs.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/users/templates/users/settings/tabs.html b/users/templates/users/settings/tabs.html index cea767b2..37278b97 100644 --- a/users/templates/users/settings/tabs.html +++ b/users/templates/users/settings/tabs.html @@ -2,9 +2,7 @@ - {# TODO: disable dropdown btn leave team if user is the only manager #} + {# TODO: @back-end disable dropdown btn leave team if user is the only manager #}
- {% if user.team_users %} -
-
- {# TODO: @back-end process Select Team with Django #} - * - -
-
-
- {% endif %} - - {% for field in extension_form %} - {% if field != 'tags' %} - {# TODO: fix handling of tags #} -
-
- {% include "common/components/field.html" with placeholder="Enter the text here..." %} -
-
- {% endif %} - {% endfor %} + {% include "extensions/components/extension_form.html" with extension_form=extension_form %}
diff --git a/extensions/templates/extensions/manage/update.html b/extensions/templates/extensions/manage/update.html index 96f85efc..da8a1eed 100644 --- a/extensions/templates/extensions/manage/update.html +++ b/extensions/templates/extensions/manage/update.html @@ -1,6 +1,5 @@ {% extends "common/base.html" %} -{% load filters %} -{% load i18n common pipeline %} +{% load common filters i18n pipeline %} {% block page_title %}{{ extension.name }}{% endblock page_title %} {% block content %} @@ -29,33 +28,7 @@ {{ form.errors }}
- {% if user.team_users %} -
-
- {# TODO: @back-end process Assign Team with Django #} - * - -
-
-
- {% endif %} - -
- {% include "common/components/field.html" with field=form.description label="Description" placeholder="Describe the extension..." %} -
- -
- {% include "common/components/field.html" with field=form.support placeholder="https://example.com" %} -
+ {% include "extensions/components/extension_form.html" with extension_form=form %}
-- 2.30.2 From c3abe491c9c9d39f75d972203ee56fff8475e783 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 17 May 2024 18:41:49 +0200 Subject: [PATCH 12/31] allow team members to edit extensions, show them in "my extensions" --- extensions/views/manage.py | 20 +++++++++++++++++++- extensions/views/mixins.py | 7 ++++++- extensions/views/public.py | 2 ++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/extensions/views/manage.py b/extensions/views/manage.py index 1a37298c..657d53db 100644 --- a/extensions/views/manage.py +++ b/extensions/views/manage.py @@ -2,6 +2,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.messages.views import SuccessMessageMixin from django.db import transaction +from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, reverse from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView @@ -99,7 +100,24 @@ class ManageListView(LoginRequiredMixin, ListView): template_name = 'extensions/manage/list.html' def get_queryset(self): - return Extension.objects.authored_by(user_id=self.request.user.pk) + filter = Q(maintainer__user_id=self.request.user.pk) + user_teams = self.request.user.teams.all() + if user_teams: + filter = filter | Q(team__in=[t.pk for t in user_teams]) + return ( + Extension.objects.filter(filter) + .prefetch_related( + 'authors', + 'preview_set', + 'preview_set__file', + 'ratings', + 'team', + 'versions', + 'versions__file', + 'versions__tags', + ) + .distinct() + ) class UpdateExtensionView( diff --git a/extensions/views/mixins.py b/extensions/views/mixins.py index 568f26ae..3ca0c603 100644 --- a/extensions/views/mixins.py +++ b/extensions/views/mixins.py @@ -1,4 +1,5 @@ from django.contrib.auth.mixins import UserPassesTestMixin +from django.db.models import Q from django.shortcuts import get_object_or_404 from extensions.models import Extension @@ -31,8 +32,12 @@ class MaintainedExtensionMixin: """Fetch an extension by slug if current user is a maintainer.""" def dispatch(self, *args, **kwargs): + filter = Q(maintainer__user_id=self.request.user.pk) + user_teams = self.request.user.teams.all() + if user_teams: + filter = filter | Q(team__in=[t.pk for t in user_teams]) self.extension = get_object_or_404( - Extension.objects.authored_by(user_id=self.request.user.pk), + Extension.objects.filter(filter), slug=self.kwargs['slug'], ) return super().dispatch(*args, **kwargs) diff --git a/extensions/views/public.py b/extensions/views/public.py index 32e65de7..3ec54025 100644 --- a/extensions/views/public.py +++ b/extensions/views/public.py @@ -50,6 +50,7 @@ class HomeView(ListedExtensionsView): 'preview_set', 'preview_set__file', 'ratings', + 'team', 'versions', 'versions__file', 'versions__tags', @@ -107,6 +108,7 @@ class SearchView(ListedExtensionsView): 'preview_set', 'preview_set__file', 'ratings', + 'team', 'versions', 'versions__file', 'versions__tags', -- 2.30.2 From 1e755b112f354545a09dd131a1b2309cc192edbc Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 17 May 2024 19:30:46 +0200 Subject: [PATCH 13/31] erase extension.team field if it has no maintainers from the team checked when users leave a team --- teams/models.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/teams/models.py b/teams/models.py index eb3f3e2b..b859513e 100644 --- a/teams/models.py +++ b/teams/models.py @@ -1,7 +1,7 @@ import logging from django.contrib.auth import get_user_model -from django.db import models +from django.db import models, transaction from django.urls import reverse from common.model_mixins import CreatedModifiedMixin @@ -49,3 +49,22 @@ class TeamsUsers(CreatedModifiedMixin, models.Model): @property def is_manager(self) -> bool: return self.role == TEAM_ROLE_MANAGER + + @transaction.atomic + def delete(self): + # erase extension.team field if the user was the last maintainer from the team + for extension in self.user.extensions.filter(team=self.team).all(): + # assuming small datasets, not optimizing db access + authors = extension.authors.all() + has_other_authors_from_the_team = False + for author in authors: + if author == self.user: + continue + if self.team in author.teams.all(): + has_other_authors_from_the_team = True + break + if not has_other_authors_from_the_team: + extension.team = None + extension.save(update_fields={'team'}) + + return super().delete() -- 2.30.2 From 55394ca8351bdc04ffd1fa9a7e581943150dbaf4 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 13:19:41 +0200 Subject: [PATCH 14/31] has_maintainer: treat team members as maintainers --- extensions/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/models.py b/extensions/models.py index 4e477b57..82537a90 100644 --- a/extensions/models.py +++ b/extensions/models.py @@ -385,10 +385,14 @@ class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Mod ) def has_maintainer(self, user) -> bool: - """Return True if given user is listed as a maintainer.""" + """Return True if given user is listed as a maintainer or is a member of the team.""" if user is None or user.is_anonymous: return False - return user in self.authors.all() + if user in self.authors.all(): + return True + if self.team and user in self.team.users.all(): + return True + return False def can_rate(self, user) -> bool: """Return True if given user can rate this extension. -- 2.30.2 From 94b324e4ef71c2725c7912f4c6d650056ceebf40 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 13:28:52 +0200 Subject: [PATCH 15/31] fix MultipleObjectsReturned in MaintainedExtensionMixin --- extensions/views/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/views/mixins.py b/extensions/views/mixins.py index 3ca0c603..f80cf54a 100644 --- a/extensions/views/mixins.py +++ b/extensions/views/mixins.py @@ -37,7 +37,7 @@ class MaintainedExtensionMixin: if user_teams: filter = filter | Q(team__in=[t.pk for t in user_teams]) self.extension = get_object_or_404( - Extension.objects.filter(filter), + Extension.objects.filter(filter).distinct(), slug=self.kwargs['slug'], ) return super().dispatch(*args, **kwargs) -- 2.30.2 From fd05069c6d5076e89a51890bb76bb1e6d9ac9246 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 14:27:20 +0200 Subject: [PATCH 16/31] force team choice in draft view --- extensions/forms.py | 10 +++++++++- extensions/views/manage.py | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/extensions/forms.py b/extensions/forms.py index fbd8ed48..986c58bb 100644 --- a/extensions/forms.py +++ b/extensions/forms.py @@ -117,6 +117,7 @@ class ExtensionUpdateForm(forms.ModelForm): ) msg_need_previews = _('Please add at least one preview.') msg_duplicate_file = _('Please select another file instead of the duplicate.') + invalid_team_value = '-1' class Meta: model = extensions.models.Extension @@ -128,6 +129,7 @@ class ExtensionUpdateForm(forms.ModelForm): def __init__(self, *args, **kwargs): """Pass the request and initialise all the nested form(set)s.""" self.request = kwargs.pop('request') + self.team_choices_add_empty = kwargs.pop('team_choices_add_empty', False) super().__init__(*args, **kwargs) if self.request.POST: edit_preview_formset = EditPreviewFormSet( @@ -168,8 +170,11 @@ class ExtensionUpdateForm(forms.ModelForm): team_pk = None if self.instance.team: team_pk = self.instance.team.pk + choices = [(None, self.request.user), *[(team.pk, team.name) for team in user_teams]] + if self.team_choices_add_empty: + choices.insert(0, (self.invalid_team_value, '-----')) self.fields['team'] = forms.ChoiceField( - choices=[(None, self.request.user), *[(team.pk, team.name) for team in user_teams]], + choices=choices, required=False, initial=team_pk, ) @@ -220,6 +225,9 @@ class ExtensionUpdateForm(forms.ModelForm): return team_pk = self.cleaned_data['team'] + if team_pk == self.invalid_team_value: + self.add_error('team', _('Please select a value')) + return if team_pk: team = self.request.user.teams.filter(pk=team_pk).first() if not team: diff --git a/extensions/views/manage.py b/extensions/views/manage.py index 657d53db..bcd1ea12 100644 --- a/extensions/views/manage.py +++ b/extensions/views/manage.py @@ -372,7 +372,11 @@ class DraftExtensionView( """Add all the additional forms to the context.""" context = super().get_context_data(**kwargs) if not extension_form: - extension_form = ExtensionUpdateForm(instance=self.extension, request=self.request) + extension_form = ExtensionUpdateForm( + instance=self.extension, + request=self.request, + team_choices_add_empty=True, + ) context['extension_form'] = extension_form context['edit_preview_formset'] = extension_form.edit_preview_formset context['add_preview_formset'] = extension_form.add_preview_formset @@ -384,7 +388,11 @@ class DraftExtensionView( """Handle bound forms and valid/invalid logic with the extra forms.""" form = self.get_form() extension_form = ExtensionUpdateForm( - self.request.POST, self.request.FILES, instance=self.extension, request=self.request + self.request.POST, + self.request.FILES, + instance=self.extension, + request=self.request, + team_choices_add_empty=True, ) if form.is_valid() and extension_form.is_valid(): return self.form_valid(form, extension_form) -- 2.30.2 From 85f89228377412d670997a332e4b99ddc928e4fb Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 15:02:10 +0200 Subject: [PATCH 17/31] let team members view detail page when an extension is unlisted --- extensions/models.py | 18 ++++++++++++------ extensions/views/mixins.py | 2 +- extensions/views/submit.py | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/extensions/models.py b/extensions/models.py index 82537a90..b7846a34 100644 --- a/extensions/models.py +++ b/extensions/models.py @@ -128,13 +128,19 @@ class ExtensionManager(models.Manager): def unlisted(self): return self.exclude(status=self.model.STATUSES.APPROVED) - def authored_by(self, user_id: int): - return self.filter(maintainer__user_id=user_id) + def authored_by(self, user): + filter = Q(maintainer__user_id=user.pk) + user_teams = user.teams.all() + if user_teams: + filter = filter | Q(team__in=[t.pk for t in user_teams]) + return self.filter(filter).distinct() - def listed_or_authored_by(self, user_id: int): - return self.filter( - Q(status=self.model.STATUSES.APPROVED) | Q(maintainer__user_id=user_id) - ).distinct() + def listed_or_authored_by(self, user): + filter = Q(status=self.model.STATUSES.APPROVED) | Q(maintainer__user_id=user.pk) + user_teams = user.teams.all() + if user_teams: + filter = filter | Q(team__in=[t.pk for t in user_teams]) + return self.filter(filter).distinct() class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model): diff --git a/extensions/views/mixins.py b/extensions/views/mixins.py index f80cf54a..d1044a79 100644 --- a/extensions/views/mixins.py +++ b/extensions/views/mixins.py @@ -24,7 +24,7 @@ class ExtensionQuerysetMixin: if self.request.user.is_staff: return Extension.objects.all() if self.request.user.is_authenticated: - return Extension.objects.listed_or_authored_by(user_id=self.request.user.pk) + return Extension.objects.listed_or_authored_by(user=self.request.user) return Extension.objects.listed diff --git a/extensions/views/submit.py b/extensions/views/submit.py index 1e530598..ea4b916a 100644 --- a/extensions/views/submit.py +++ b/extensions/views/submit.py @@ -18,7 +18,7 @@ class UploadFileView(LoginRequiredMixin, CreateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - drafts = Extension.objects.authored_by(user_id=self.request.user.pk).filter( + drafts = Extension.objects.authored_by(user=self.request.user).filter( status=Extension.STATUSES.INCOMPLETE ) context['drafts'] = drafts @@ -41,7 +41,7 @@ class UploadFileView(LoginRequiredMixin, CreateView): if parsed_extension_fields: # Try to look up extension by the same author and file info extension = ( - Extension.objects.authored_by(user_id=self.request.user.pk) + Extension.objects.authored_by(user=self.request.user) .filter(type=self.file.type, **parsed_extension_fields) .first() ) -- 2.30.2 From 55889b106d75e55446a118b6d4df7c9745f8f76f Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 15:24:08 +0200 Subject: [PATCH 18/31] minor refactoring --- extensions/forms.py | 4 ++-- extensions/models.py | 13 ++++++------- extensions/views/manage.py | 4 ++-- extensions/views/mixins.py | 2 +- extensions/views/submit.py | 8 +++++--- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/extensions/forms.py b/extensions/forms.py index 986c58bb..3d602803 100644 --- a/extensions/forms.py +++ b/extensions/forms.py @@ -129,7 +129,7 @@ class ExtensionUpdateForm(forms.ModelForm): def __init__(self, *args, **kwargs): """Pass the request and initialise all the nested form(set)s.""" self.request = kwargs.pop('request') - self.team_choices_add_empty = kwargs.pop('team_choices_add_empty', False) + self.add_invalid_team_choice = kwargs.pop('add_invalid_team_choice', False) super().__init__(*args, **kwargs) if self.request.POST: edit_preview_formset = EditPreviewFormSet( @@ -171,7 +171,7 @@ class ExtensionUpdateForm(forms.ModelForm): if self.instance.team: team_pk = self.instance.team.pk choices = [(None, self.request.user), *[(team.pk, team.name) for team in user_teams]] - if self.team_choices_add_empty: + if self.add_invalid_team_choice: choices.insert(0, (self.invalid_team_value, '-----')) self.fields['team'] = forms.ChoiceField( choices=choices, diff --git a/extensions/models.py b/extensions/models.py index b7846a34..f4a0cbd1 100644 --- a/extensions/models.py +++ b/extensions/models.py @@ -128,19 +128,18 @@ class ExtensionManager(models.Manager): def unlisted(self): return self.exclude(status=self.model.STATUSES.APPROVED) - def authored_by(self, user): + def _authored_by_filter(self, user): filter = Q(maintainer__user_id=user.pk) user_teams = user.teams.all() if user_teams: filter = filter | Q(team__in=[t.pk for t in user_teams]) - return self.filter(filter).distinct() + return filter + + def authored_by(self, user): + return self.filter(self._authored_by_filter(user)) def listed_or_authored_by(self, user): - filter = Q(status=self.model.STATUSES.APPROVED) | Q(maintainer__user_id=user.pk) - user_teams = user.teams.all() - if user_teams: - filter = filter | Q(team__in=[t.pk for t in user_teams]) - return self.filter(filter).distinct() + return self.filter(Q(status=self.model.STATUSES.APPROVED) | self._authored_by_filter(user)) class Extension(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, models.Model): diff --git a/extensions/views/manage.py b/extensions/views/manage.py index bcd1ea12..8fa38b16 100644 --- a/extensions/views/manage.py +++ b/extensions/views/manage.py @@ -375,7 +375,7 @@ class DraftExtensionView( extension_form = ExtensionUpdateForm( instance=self.extension, request=self.request, - team_choices_add_empty=True, + add_invalid_team_choice=True, ) context['extension_form'] = extension_form context['edit_preview_formset'] = extension_form.edit_preview_formset @@ -392,7 +392,7 @@ class DraftExtensionView( self.request.FILES, instance=self.extension, request=self.request, - team_choices_add_empty=True, + add_invalid_team_choice=True, ) if form.is_valid() and extension_form.is_valid(): return self.form_valid(form, extension_form) diff --git a/extensions/views/mixins.py b/extensions/views/mixins.py index d1044a79..754dab7f 100644 --- a/extensions/views/mixins.py +++ b/extensions/views/mixins.py @@ -24,7 +24,7 @@ class ExtensionQuerysetMixin: if self.request.user.is_staff: return Extension.objects.all() if self.request.user.is_authenticated: - return Extension.objects.listed_or_authored_by(user=self.request.user) + return Extension.objects.listed_or_authored_by(self.request.user).distinct() return Extension.objects.listed diff --git a/extensions/views/submit.py b/extensions/views/submit.py index ea4b916a..87830bc0 100644 --- a/extensions/views/submit.py +++ b/extensions/views/submit.py @@ -18,8 +18,10 @@ class UploadFileView(LoginRequiredMixin, CreateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - drafts = Extension.objects.authored_by(user=self.request.user).filter( - status=Extension.STATUSES.INCOMPLETE + drafts = ( + Extension.objects.authored_by(self.request.user) + .filter(status=Extension.STATUSES.INCOMPLETE) + .distinct() ) context['drafts'] = drafts return context @@ -41,7 +43,7 @@ class UploadFileView(LoginRequiredMixin, CreateView): if parsed_extension_fields: # Try to look up extension by the same author and file info extension = ( - Extension.objects.authored_by(user=self.request.user) + Extension.objects.authored_by(self.request.user) .filter(type=self.file.type, **parsed_extension_fields) .first() ) -- 2.30.2 From 523628739c0f4c8a319ddcbe9c306346240ab0ba Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 16:21:32 +0200 Subject: [PATCH 19/31] leave-team page --- teams/templates/teams/confirm_leave.html | 38 ++++++++++++++++++++++++ teams/templates/teams/team_list.html | 4 +-- teams/urls.py | 5 ++++ teams/views.py | 27 +++++++++++++++-- 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 teams/templates/teams/confirm_leave.html diff --git a/teams/templates/teams/confirm_leave.html b/teams/templates/teams/confirm_leave.html new file mode 100644 index 00000000..f74acc43 --- /dev/null +++ b/teams/templates/teams/confirm_leave.html @@ -0,0 +1,38 @@ +{% extends "common/base.html" %} +{% load i18n %} +{% block content %} +
+
+
+

+ {% blocktranslate with team_name=object.name %}Leave team {{ team_name }}?{% endblocktranslate %} +

+

+ {% if will_lose_access_to %} + {% blocktranslate %} + You will lose access to all team extensions that were not uploaded by you: + {% endblocktranslate %} +

    + {% for extension in will_lose_access_to %} +
  • {{ extension }}
  • + {% endfor %} +
+ {% endif %} +

+
+ + + {% trans 'Cancel' %} + +
+ {% csrf_token %} + +
+
+
+
+
+{% endblock content %} diff --git a/teams/templates/teams/team_list.html b/teams/templates/teams/team_list.html index b5035445..9af90057 100644 --- a/teams/templates/teams/team_list.html +++ b/teams/templates/teams/team_list.html @@ -45,9 +45,7 @@ diff --git a/teams/urls.py b/teams/urls.py index 98d883dc..3b0fada3 100644 --- a/teams/urls.py +++ b/teams/urls.py @@ -5,4 +5,9 @@ import teams.views app_name = 'teams' urlpatterns = [ path('settings/teams/', teams.views.TeamsView.as_view(), name='list'), + path( + 'settings/leave-team//', + teams.views.LeaveTeamView.as_view(), + name='leave-team', + ), ] diff --git a/teams/views.py b/teams/views.py index ef792c84..a3a56072 100644 --- a/teams/views.py +++ b/teams/views.py @@ -1,12 +1,35 @@ """Team pages.""" from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import redirect from django.views.generic import ListView +from django.views.generic.detail import DetailView -import teams.models +from extensions.models import Extension +from teams.models import Team, TeamsUsers class TeamsView(LoginRequiredMixin, ListView): - model = teams.models.Team + model = Team def get_queryset(self): return self.request.user.teams.all() + + +class LeaveTeamView(LoginRequiredMixin, DetailView): + model = Team + template_name = 'teams/confirm_leave.html' + + def post(self, request, *args, **kwargs): + team = self.get_object() + team_user = TeamsUsers.objects.filter(team=team, user=self.request.user).first() + team_user.delete() + return redirect('teams:list') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['will_lose_access_to'] = list( + Extension.objects.authored_by(self.request.user) + .exclude(maintainer__user_id=self.request.user.pk) + .all() + ) + return context -- 2.30.2 From 77828287bfe9a2af6906a90ab8b1e5bacac78d9a Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 16:23:37 +0200 Subject: [PATCH 20/31] prevent 500 error when a form is submitted twice --- teams/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/teams/views.py b/teams/views.py index a3a56072..06d1a683 100644 --- a/teams/views.py +++ b/teams/views.py @@ -22,7 +22,8 @@ class LeaveTeamView(LoginRequiredMixin, DetailView): def post(self, request, *args, **kwargs): team = self.get_object() team_user = TeamsUsers.objects.filter(team=team, user=self.request.user).first() - team_user.delete() + if team_user: + team_user.delete() return redirect('teams:list') def get_context_data(self, **kwargs): -- 2.30.2 From 143cf860f591ea49c5ff8c2c5eea2b7ab811e22d Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 17:08:45 +0200 Subject: [PATCH 21/31] disable "leave team" link when the user is the only manager --- teams/models.py | 14 ++++++++++++++ teams/templates/teams/team_list.html | 14 ++++++++------ teams/views.py | 6 ++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/teams/models.py b/teams/models.py index b859513e..fcf00d4c 100644 --- a/teams/models.py +++ b/teams/models.py @@ -68,3 +68,17 @@ class TeamsUsers(CreatedModifiedMixin, models.Model): extension.save(update_fields={'team'}) return super().delete() + + def may_leave(self) -> bool: + nr_of_managers = TeamsUsers.objects.filter(role=TEAM_ROLE_MANAGER, team=self.team).count() + user_is_manager = ( + TeamsUsers.objects.filter( + role=TEAM_ROLE_MANAGER, + team=self.team, + user=self.user, + ).first() + is not None + ) + if user_is_manager and nr_of_managers < 2: + return False + return True diff --git a/teams/templates/teams/team_list.html b/teams/templates/teams/team_list.html index 9af90057..09e5f0d1 100644 --- a/teams/templates/teams/team_list.html +++ b/teams/templates/teams/team_list.html @@ -4,7 +4,7 @@

Teams

- {% if user.team_users.all %} + {% if team_memberships %} @@ -21,7 +21,7 @@ - {% for team_member in user.team_users.all %} + {% for team_member in team_memberships %} {% with team=team_member.team %}
@@ -38,14 +38,16 @@ - {# TODO: @back-end disable dropdown btn leave team if user is the only manager #} diff --git a/teams/views.py b/teams/views.py index 06d1a683..b1e9d4f9 100644 --- a/teams/views.py +++ b/teams/views.py @@ -11,8 +11,10 @@ from teams.models import Team, TeamsUsers class TeamsView(LoginRequiredMixin, ListView): model = Team - def get_queryset(self): - return self.request.user.teams.all() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['team_memberships'] = self.request.user.team_users.select_related('team').all() + return context class LeaveTeamView(LoginRequiredMixin, DetailView): -- 2.30.2 From 173cae7694cf4ec91ba34cf928c5fc224484edda Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 17:11:05 +0200 Subject: [PATCH 22/31] order teams by their name --- teams/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/teams/views.py b/teams/views.py index b1e9d4f9..c82481cc 100644 --- a/teams/views.py +++ b/teams/views.py @@ -13,7 +13,9 @@ class TeamsView(LoginRequiredMixin, ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['team_memberships'] = self.request.user.team_users.select_related('team').all() + context['team_memberships'] = ( + self.request.user.team_users.select_related('team').order_by('team__name').all() + ) return context -- 2.30.2 From 0bbc95a92d97977a134e27afd59f26d038dbf6a2 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 17:45:41 +0200 Subject: [PATCH 23/31] test for team leaving --- teams/models.py | 1 + teams/templates/teams/confirm_leave.html | 6 +++++ teams/tests/test_leave.py | 33 ++++++++++++++++++++++++ teams/views.py | 5 +++- 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 teams/tests/test_leave.py diff --git a/teams/models.py b/teams/models.py index fcf00d4c..4c5f65b7 100644 --- a/teams/models.py +++ b/teams/models.py @@ -69,6 +69,7 @@ class TeamsUsers(CreatedModifiedMixin, models.Model): return super().delete() + @property def may_leave(self) -> bool: nr_of_managers = TeamsUsers.objects.filter(role=TEAM_ROLE_MANAGER, team=self.team).count() user_is_manager = ( diff --git a/teams/templates/teams/confirm_leave.html b/teams/templates/teams/confirm_leave.html index f74acc43..ae907eb9 100644 --- a/teams/templates/teams/confirm_leave.html +++ b/teams/templates/teams/confirm_leave.html @@ -7,6 +7,7 @@

{% blocktranslate with team_name=object.name %}Leave team {{ team_name }}?{% endblocktranslate %}

+ {% if may_leave %}

{% if will_lose_access_to %} {% blocktranslate %} @@ -32,6 +33,11 @@ + {% else %} +

+ {% trans 'You cannot leave this team because you are the only manager.' %} +

+ {% endif %} diff --git a/teams/tests/test_leave.py b/teams/tests/test_leave.py new file mode 100644 index 00000000..5de5ab48 --- /dev/null +++ b/teams/tests/test_leave.py @@ -0,0 +1,33 @@ +from django.test import TestCase +from django.urls import reverse + + +from common.tests.factories.teams import TeamFactory +from common.tests.factories.users import UserFactory +from constants.base import TEAM_ROLE_MANAGER +from teams.models import TeamsUsers + + +class TeamLeaveTest(TestCase): + def test_the_only_manager_cant_leave(self): + team = TeamFactory(slug='test-team') + user = UserFactory() + TeamsUsers(team=team, user=user, role=TEAM_ROLE_MANAGER).save() + self.assertEqual(user.teams.count(), 1) + + self.client.force_login(user) + response = self.client.get(reverse('teams:leave-team', args=[team.slug])) + self.assertContains(response, 'cannot leave') + self.client.post(reverse('teams:leave-team', args=[team.slug])) + user.refresh_from_db() + self.assertEqual(user.teams.count(), 1) + + # create another manager + user2 = UserFactory() + TeamsUsers(team=team, user=user2, role=TEAM_ROLE_MANAGER).save() + # try to leave again + response = self.client.get(reverse('teams:leave-team', args=[team.slug])) + self.assertNotContains(response, 'cannot leave') + self.client.post(reverse('teams:leave-team', args=[team.slug])) + user.refresh_from_db() + self.assertEqual(user.teams.count(), 0) diff --git a/teams/views.py b/teams/views.py index c82481cc..27640e89 100644 --- a/teams/views.py +++ b/teams/views.py @@ -26,12 +26,15 @@ class LeaveTeamView(LoginRequiredMixin, DetailView): def post(self, request, *args, **kwargs): team = self.get_object() team_user = TeamsUsers.objects.filter(team=team, user=self.request.user).first() - if team_user: + if team_user and team_user.may_leave: team_user.delete() return redirect('teams:list') def get_context_data(self, **kwargs): + team = self.get_object() + team_user = TeamsUsers.objects.filter(team=team, user=self.request.user).first() context = super().get_context_data(**kwargs) + context['may_leave'] = team_user.may_leave context['will_lose_access_to'] = list( Extension.objects.authored_by(self.request.user) .exclude(maintainer__user_id=self.request.user.pk) -- 2.30.2 From 9b5a5bf976a1601401b46951b6025ff3465492ee Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 17:50:47 +0200 Subject: [PATCH 24/31] test for extension.team reset --- teams/tests/test_leave.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/teams/tests/test_leave.py b/teams/tests/test_leave.py index 5de5ab48..949ea4c7 100644 --- a/teams/tests/test_leave.py +++ b/teams/tests/test_leave.py @@ -2,9 +2,10 @@ from django.test import TestCase from django.urls import reverse +from common.tests.factories.extensions import create_version from common.tests.factories.teams import TeamFactory from common.tests.factories.users import UserFactory -from constants.base import TEAM_ROLE_MANAGER +from constants.base import TEAM_ROLE_MANAGER, TEAM_ROLE_MEMBER from teams.models import TeamsUsers @@ -31,3 +32,21 @@ class TeamLeaveTest(TestCase): self.client.post(reverse('teams:leave-team', args=[team.slug])) user.refresh_from_db() self.assertEqual(user.teams.count(), 0) + + def test_extensions_lose_team_assignment(self): + team = TeamFactory(slug='test-team') + user = UserFactory() + TeamsUsers(team=team, user=user, role=TEAM_ROLE_MEMBER).save() + + extension = create_version().extension + extension.team = team + extension.authors.add(user) + extension.save() + + self.client.force_login(user) + self.client.post(reverse('teams:leave-team', args=[team.slug])) + user.refresh_from_db() + self.assertEqual(user.teams.count(), 0) + + extension.refresh_from_db() + self.assertIsNone(extension.team) -- 2.30.2 From 4278edd2b5ac34d62d090d9c543e41058895f7a3 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 18:10:00 +0200 Subject: [PATCH 25/31] test basic visibility of extension pages for team members --- extensions/tests/test_views.py | 49 +++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/extensions/tests/test_views.py b/extensions/tests/test_views.py index 5bf466e6..80c7a97c 100644 --- a/extensions/tests/test_views.py +++ b/extensions/tests/test_views.py @@ -4,10 +4,11 @@ from django.test import TestCase from django.urls import reverse from common.tests.factories.extensions import create_version, create_approved_version +from common.tests.factories.teams import TeamFactory from common.tests.factories.users import UserFactory from extensions.models import Extension, Version from files.models import File -from teams.models import Team +from teams.models import Team, TeamsUsers def _create_extension(): @@ -190,7 +191,7 @@ class ExtensionDetailViewTest(_BaseTestCase): self._check_detail_page(response, extension) - def test_can_view_unlisted_extension_if_maintaner(self): + def test_can_view_unlisted_extension_if_maintainer(self): extension = _create_extension() self.client.force_login(extension.authors.first()) @@ -198,6 +199,20 @@ class ExtensionDetailViewTest(_BaseTestCase): self._check_detail_page(response, extension) + def test_can_view_unlisted_extension_if_team_member(self): + extension = _create_extension() + + team = TeamFactory(slug='test-team') + user = UserFactory() + TeamsUsers(team=team, user=user).save() + extension.team = team + extension.save() + + self.client.force_login(user) + response = self.client.get(extension.get_manage_url()) + + self._check_detail_page(response, extension) + def test_can_view_publicly_listed_extension_anonymously(self): extension = _create_extension() extension.approve() @@ -245,7 +260,7 @@ class ExtensionManageViewTest(_BaseTestCase): self.assertEqual(response.status_code, 302) - def test_can_view_manage_extension_page_if_maintaner(self): + def test_can_view_manage_extension_page_if_maintainer(self): extension = _create_extension() extension.approve() @@ -254,6 +269,20 @@ class ExtensionManageViewTest(_BaseTestCase): self._check_manage_page(response, extension) + def test_can_view_manage_extension_page_if_team_member(self): + extension = _create_extension() + extension.approve() + team = TeamFactory(slug='test-team') + user = UserFactory() + TeamsUsers(team=team, user=user).save() + extension.team = team + extension.save() + + self.client.force_login(user) + response = self.client.get(extension.get_manage_url()) + + self._check_manage_page(response, extension) + class ListedExtensionsTest(_BaseTestCase): def setUp(self): @@ -354,3 +383,17 @@ class UpdateVersionViewTest(_BaseTestCase): self.assertEqual(response2.status_code, 302) version.refresh_from_db() self.assertEqual(version.blender_version_max, '4.2.0') + + +class MyExtensionsTest(_BaseTestCase): + def test_team_members_see_extensions_in_my_extensions(self): + extension = _create_extension() + team = TeamFactory(slug='test-team') + user = UserFactory() + TeamsUsers(team=team, user=user).save() + extension.team = team + extension.save() + + self.client.force_login(user) + response = self.client.get(reverse('extensions:manage-list')) + self.assertContains(response, extension.name) -- 2.30.2 From d6dc8c3c68475bf7d4d471731856e63d5415cb5a Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Tue, 21 May 2024 18:17:03 +0200 Subject: [PATCH 26/31] remove forgotten debug --- teams/templates/teams/team_list.html | 1 - 1 file changed, 1 deletion(-) diff --git a/teams/templates/teams/team_list.html b/teams/templates/teams/team_list.html index 09e5f0d1..2fa22883 100644 --- a/teams/templates/teams/team_list.html +++ b/teams/templates/teams/team_list.html @@ -42,7 +42,6 @@ - {{ team.user_may_leave }}