See #248: can now filter on invitation status and delete invitations
This commit is contained in:
parent
7b0148a533
commit
7cfa61292a
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue