diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 16ee5c162f3e1ce78da587e6e68fbbefb370f164..5f83ebf1a3143e201891448e2f43206c14015d7c 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -40,7 +40,13 @@ class ManageUserFilterSet(filters.FilterSet): class ManageInvitationFilterSet(filters.FilterSet): q = fields.SearchFilter(search_fields=["owner__username", "code", "owner__email"]) + is_open = filters.BooleanFilter(method="filter_is_open") class Meta: model = users_models.Invitation - fields = ["q"] + fields = ["q", "is_open"] + + def filter_is_open(self, queryset, field_name, value): + if value is None: + return queryset + return queryset.open(value) diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index f5d52bcace6a6cde9f751ff987af214b6134ea2d..d1a9ebb84b75f97873a157e48e48199aa708cfbb 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -151,3 +151,12 @@ class ManageInvitationSerializer(serializers.ModelSerializer): "An invitation with this code already exists" ) return value + + +class ManageInvitationActionSerializer(common_serializers.ActionSerializer): + actions = [common_serializers.Action("delete", allow_all=False)] + filterset_class = filters.ManageInvitationFilterSet + + @transaction.atomic + def handle_delete(self, objects): + return objects.delete() diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 803f8db7c3a76fd70b3d6c0b310329b89eb7bed7..ae3c08a57c829dbc8330c37568a3043fa5f8484e 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -86,3 +86,13 @@ class ManageInvitationViewSet( def perform_create(self, serializer): serializer.save(owner=self.request.user) + + @list_route(methods=["post"]) + def action(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = serializers.ManageInvitationActionSerializer( + request.data, queryset=queryset + ) + serializer.is_valid(raise_exception=True) + result = serializer.save() + return response.Response(result, status=200) diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index 61f57a3c55dcced4af65586b2c20012b6af6f391..e205d04d7fdbe12ca122b27f76c00c438f70347a 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -157,12 +157,13 @@ def generate_code(length=10): class InvitationQuerySet(models.QuerySet): - def open(self): + def open(self, include=True): now = timezone.now() qs = self.annotate(_users=models.Count("users")) - qs = qs.filter(_users=0) - qs = qs.exclude(expiration_date__lte=now) - return qs + query = models.Q(_users=0, expiration_date__gt=now) + if include: + return qs.filter(query) + return qs.exclude(query) class Invitation(models.Model): diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index 475691293dd347cd81946743994cb20fef62be09..ea760cc6c6b5a49f39903f1d641bd3d664819348 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -118,3 +118,12 @@ def test_can_filter_open_invitations(factories): assert models.Invitation.objects.count() == 3 assert list(models.Invitation.objects.open()) == [okay] + + +def test_can_filter_closed_invitations(factories): + factories["users.Invitation"]() + expired = factories["users.Invitation"](expired=True) + used = factories["users.User"](invited=True).invitation + + assert models.Invitation.objects.count() == 3 + assert list(models.Invitation.objects.open(False)) == [expired, used] diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue index f23479066e37667d3ea7dba63d504f1778a93d07..097fb29385eb495d00bc349d88f733c5d9ac5813 100644 --- a/front/src/components/common/ActionTable.vue +++ b/front/src/components/common/ActionTable.vue @@ -36,7 +36,7 @@ <div class="count field"> <span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span> <span v-else>{{ $t('{% count %} on {% total %} selected', {count: checked.length, total: objectsData.count}) }}</span> - <template v-if="!currentAction.isDangerous && checkable.length === checked.length"> + <template v-if="!currentAction.isDangerous && checkable.length > 0 && checkable.length === checked.length"> <a @click="selectAll = true" v-if="!selectAll"> {{ $t('Select all {% total %} elements', {total: objectsData.count}) }} </a> @@ -157,6 +157,7 @@ export default { let self = this self.actionLoading = true self.result = null + self.actionErrors = [] let payload = { action: this.currentActionName, filters: this.filters diff --git a/front/src/components/manage/users/InvitationForm.vue b/front/src/components/manage/users/InvitationForm.vue index ffd5a7d1267770dce3276e8224f9951cfc2415ff..9429c1ae16294c49b91aa5a6ee43780a7b596c90 100644 --- a/front/src/components/manage/users/InvitationForm.vue +++ b/front/src/components/manage/users/InvitationForm.vue @@ -1,6 +1,6 @@ <template> <div> - <form v-if="!over" class="ui form" @submit.prevent="submit"> + <form class="ui form" @submit.prevent="submit"> <div v-if="errors.length > 0" class="ui negative message"> <div class="header">{{ $t('Error while creating invitation') }}</div> <ul class="list"> diff --git a/front/src/components/manage/users/InvitationsTable.vue b/front/src/components/manage/users/InvitationsTable.vue index e9b46cc2c96ea5e1c75bc29b5d4df77830917aa2..e8d0a2406aaf465d7e30803b05ace126b621606d 100644 --- a/front/src/components/manage/users/InvitationsTable.vue +++ b/front/src/components/manage/users/InvitationsTable.vue @@ -7,7 +7,7 @@ <input type="text" v-model="search" placeholder="Search by username, email, code..." /> </div> <div class="field"> - <i18next tag="label" path="Ordering"/> + <label>{{ $t("Ordering") }}</label> <select class="ui dropdown" v-model="ordering"> <option v-for="option in orderingOptions" :value="option[0]"> {{ option[1] }} @@ -15,10 +15,11 @@ </select> </div> <div class="field"> - <i18next tag="label" path="Ordering direction"/> - <select class="ui dropdown" v-model="orderingDirection"> - <option value="+">{{ $t('Ascending') }}</option> - <option value="-">{{ $t('Descending') }}</option> + <label>{{ $t("Status") }}</label> + <select class="ui dropdown" v-model="isOpen"> + <option :value="null">{{ $t('All') }}</option> + <option :value="true">{{ $t('Open') }}</option> + <option :value="false">{{ $t('Expired/used') }}</option> </select> </div> </div> @@ -47,7 +48,7 @@ </td> <td> <span v-if="scope.obj.users.length > 0" class="ui green basic label">{{ $t('Used') }}</span> - <span v-else-if="scope.obj.expiration_date < new Date()" class="ui red basic label">{{ $t('Expired') }}</span> + <span v-else-if="moment().isAfter(scope.obj.expiration_date)" class="ui red basic label">{{ $t('Expired') }}</span> <span v-else class="ui basic label">{{ $t('Not used') }}</span> </td> <td> @@ -81,8 +82,8 @@ <script> import axios from 'axios' +import moment from 'moment' import _ from 'lodash' -import time from '@/utils/time' import Pagination from '@/components/Pagination' import ActionTable from '@/components/common/ActionTable' import OrderingMixin from '@/components/mixins/Ordering' @@ -99,12 +100,13 @@ export default { data () { let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date') return { - time, + moment, isLoading: false, result: null, page: 1, paginateBy: 50, search: '', + isOpen: null, orderingDirection: defaultOrdering.direction || '+', ordering: defaultOrdering.field, orderingOptions: [ @@ -123,6 +125,7 @@ export default { 'page': this.page, 'page_size': this.paginateBy, 'q': this.search, + 'is_open': this.isOpen, 'ordering': this.getOrderingAsString() }, this.filters) let self = this @@ -153,11 +156,13 @@ export default { }, actions () { return [ - // { - // name: 'delete', - // label: this.$t('Delete'), - // isDangerous: true - // } + { + name: 'delete', + label: this.$t('Delete'), + filterCheckable: (obj) => { + return obj.users.length === 0 && moment().isBefore(obj.expiration_date) + } + } ] } }, @@ -170,9 +175,15 @@ export default { this.fetchData() }, ordering () { + this.page = 1 + this.fetchData() + }, + isOpen () { + this.page = 1 this.fetchData() }, orderingDirection () { + this.page = 1 this.fetchData() } }