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):
|
class ManageInvitationFilterSet(filters.FilterSet):
|
||||||
q = fields.SearchFilter(search_fields=["owner__username", "code", "owner__email"])
|
q = fields.SearchFilter(search_fields=["owner__username", "code", "owner__email"])
|
||||||
|
is_open = filters.BooleanFilter(method="filter_is_open")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = users_models.Invitation
|
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"
|
"An invitation with this code already exists"
|
||||||
)
|
)
|
||||||
return value
|
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):
|
def perform_create(self, serializer):
|
||||||
serializer.save(owner=self.request.user)
|
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):
|
class InvitationQuerySet(models.QuerySet):
|
||||||
def open(self):
|
def open(self, include=True):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
qs = self.annotate(_users=models.Count("users"))
|
qs = self.annotate(_users=models.Count("users"))
|
||||||
qs = qs.filter(_users=0)
|
query = models.Q(_users=0, expiration_date__gt=now)
|
||||||
qs = qs.exclude(expiration_date__lte=now)
|
if include:
|
||||||
return qs
|
return qs.filter(query)
|
||||||
|
return qs.exclude(query)
|
||||||
|
|
||||||
|
|
||||||
class Invitation(models.Model):
|
class Invitation(models.Model):
|
||||||
|
|
|
@ -118,3 +118,12 @@ def test_can_filter_open_invitations(factories):
|
||||||
|
|
||||||
assert models.Invitation.objects.count() == 3
|
assert models.Invitation.objects.count() == 3
|
||||||
assert list(models.Invitation.objects.open()) == [okay]
|
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">
|
<div class="count field">
|
||||||
<span v-if="selectAll">{{ $t('{% count %} on {% total %} selected', {count: objectsData.count, total: objectsData.count}) }}</span>
|
<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>
|
<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">
|
<a @click="selectAll = true" v-if="!selectAll">
|
||||||
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
|
{{ $t('Select all {% total %} elements', {total: objectsData.count}) }}
|
||||||
</a>
|
</a>
|
||||||
|
@ -157,6 +157,7 @@ export default {
|
||||||
let self = this
|
let self = this
|
||||||
self.actionLoading = true
|
self.actionLoading = true
|
||||||
self.result = null
|
self.result = null
|
||||||
|
self.actionErrors = []
|
||||||
let payload = {
|
let payload = {
|
||||||
action: this.currentActionName,
|
action: this.currentActionName,
|
||||||
filters: this.filters
|
filters: this.filters
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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 v-if="errors.length > 0" class="ui negative message">
|
||||||
<div class="header">{{ $t('Error while creating invitation') }}</div>
|
<div class="header">{{ $t('Error while creating invitation') }}</div>
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<input type="text" v-model="search" placeholder="Search by username, email, code..." />
|
<input type="text" v-model="search" placeholder="Search by username, email, code..." />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<i18next tag="label" path="Ordering"/>
|
<label>{{ $t("Ordering") }}</label>
|
||||||
<select class="ui dropdown" v-model="ordering">
|
<select class="ui dropdown" v-model="ordering">
|
||||||
<option v-for="option in orderingOptions" :value="option[0]">
|
<option v-for="option in orderingOptions" :value="option[0]">
|
||||||
{{ option[1] }}
|
{{ option[1] }}
|
||||||
|
@ -15,10 +15,11 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<i18next tag="label" path="Ordering direction"/>
|
<label>{{ $t("Status") }}</label>
|
||||||
<select class="ui dropdown" v-model="orderingDirection">
|
<select class="ui dropdown" v-model="isOpen">
|
||||||
<option value="+">{{ $t('Ascending') }}</option>
|
<option :value="null">{{ $t('All') }}</option>
|
||||||
<option value="-">{{ $t('Descending') }}</option>
|
<option :value="true">{{ $t('Open') }}</option>
|
||||||
|
<option :value="false">{{ $t('Expired/used') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +48,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="scope.obj.users.length > 0" class="ui green basic label">{{ $t('Used') }}</span>
|
<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>
|
<span v-else class="ui basic label">{{ $t('Not used') }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -81,8 +82,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import time from '@/utils/time'
|
|
||||||
import Pagination from '@/components/Pagination'
|
import Pagination from '@/components/Pagination'
|
||||||
import ActionTable from '@/components/common/ActionTable'
|
import ActionTable from '@/components/common/ActionTable'
|
||||||
import OrderingMixin from '@/components/mixins/Ordering'
|
import OrderingMixin from '@/components/mixins/Ordering'
|
||||||
|
@ -99,12 +100,13 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
|
let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-creation_date')
|
||||||
return {
|
return {
|
||||||
time,
|
moment,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
result: null,
|
result: null,
|
||||||
page: 1,
|
page: 1,
|
||||||
paginateBy: 50,
|
paginateBy: 50,
|
||||||
search: '',
|
search: '',
|
||||||
|
isOpen: null,
|
||||||
orderingDirection: defaultOrdering.direction || '+',
|
orderingDirection: defaultOrdering.direction || '+',
|
||||||
ordering: defaultOrdering.field,
|
ordering: defaultOrdering.field,
|
||||||
orderingOptions: [
|
orderingOptions: [
|
||||||
|
@ -123,6 +125,7 @@ export default {
|
||||||
'page': this.page,
|
'page': this.page,
|
||||||
'page_size': this.paginateBy,
|
'page_size': this.paginateBy,
|
||||||
'q': this.search,
|
'q': this.search,
|
||||||
|
'is_open': this.isOpen,
|
||||||
'ordering': this.getOrderingAsString()
|
'ordering': this.getOrderingAsString()
|
||||||
}, this.filters)
|
}, this.filters)
|
||||||
let self = this
|
let self = this
|
||||||
|
@ -153,11 +156,13 @@ export default {
|
||||||
},
|
},
|
||||||
actions () {
|
actions () {
|
||||||
return [
|
return [
|
||||||
// {
|
{
|
||||||
// name: 'delete',
|
name: 'delete',
|
||||||
// label: this.$t('Delete'),
|
label: this.$t('Delete'),
|
||||||
// isDangerous: true
|
filterCheckable: (obj) => {
|
||||||
// }
|
return obj.users.length === 0 && moment().isBefore(obj.expiration_date)
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -170,9 +175,15 @@ export default {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
ordering () {
|
ordering () {
|
||||||
|
this.page = 1
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
isOpen () {
|
||||||
|
this.page = 1
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
orderingDirection () {
|
orderingDirection () {
|
||||||
|
this.page = 1
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue