Improved raw mode and added new filter ops

This commit is contained in:
Antoine Nguyen
2024-06-22 19:44:48 +02:00
committed by Spitfireap
parent 8a6308278c
commit f527051ca2
5 changed files with 148 additions and 42 deletions
+10
View File
@@ -94,6 +94,16 @@ export default {
`${resource}/filtersets/${filterSetName}/filters/${filter}/disable/`
)
},
moveFilterDown(filterSetName, filter) {
return repository.post(
`${resource}/filtersets/${filterSetName}/filters/${filter}/move_down/`
)
},
moveFilterUp(filterSetName, filter) {
return repository.post(
`${resource}/filtersets/${filterSetName}/filters/${filter}/move_up/`
)
},
enableFilter(filterSetName, filter) {
return repository.post(
`${resource}/filtersets/${filterSetName}/filters/${filter}/enable/`
@@ -10,8 +10,9 @@
variant="outlined"
density="compact"
:rules="[rules.required]"
:error-messages="apiErrors.name ? apiErrors.name : []"
/>
<div>
<div class="bg-grey-lighten-4 rounded pa-3">
<h4>{{ $gettext('Conditions') }}</h4>
<v-radio-group
v-model="form.match_type"
@@ -42,7 +43,6 @@
variant="outlined"
density="compact"
:rules="[rules.required]"
@update:model-value=""
/>
<v-select
v-model="condition.operator"
@@ -83,7 +83,7 @@
</div>
</template>
</div>
<div>
<div class="mt-4 bg-grey-lighten-4 rounded pa-3">
<h4>{{ $gettext('Actions') }}</h4>
<div v-for="(action, index) in form.actions" class="d-flex align-start mt-2">
<v-select
@@ -140,11 +140,12 @@
<v-spacer></v-spacer>
<v-btn
:text="$gettext('Cancel')"
variant="plain"
variant="elevated"
@click="close"
></v-btn>
<v-btn
color="primary"
variant="elevated"
:text="submitLabel"
:loading="working"
@click="submit"
@@ -175,6 +176,7 @@ const emit = defineEmits(['close'])
const busStore = useBusStore()
const { $gettext } = useGettext()
const apiErrors = ref({})
const conditionTemplates = ref([])
const actionTemplates = ref([])
const formRef = ref()
@@ -290,8 +292,8 @@ async function submit() {
msg = $gettext('Filter updated')
}
} catch (err) {
if (err.response.data && err.response.data.error) {
busStore.displayNotification({ msg: err.response.data.error, type: 'error' })
if (err.response.status === 400 && err.response.data) {
apiErrors.value = err.response.data
}
return
} finally {
+61 -21
View File
@@ -55,11 +55,11 @@
>
<template v-if="!rawMode">
<div class="d-flex align-center">
<v-spacer />
<v-btn
color="primary"
:title="$gettext('Add new filter')"
@click="showFilterForm = true"
class="mx-auto"
>
<v-icon icon="mdi-plus" />
{{ $gettext('Add') }}
@@ -73,7 +73,7 @@
<template #[`item.enabled`]="{ item }">
{{ $yesno(item.enabled) }}
</template>
<template #[`item.filter_actions`]="{ item }">
<template #[`item.filter_actions`]="{ index, item }">
<v-menu offset-y>
<template #activator="{ props }">
<v-btn
@@ -83,21 +83,26 @@
>
</v-btn>
</template>
<MenuItems :items="getFilterActions(item)" :obj="item" />
<MenuItems :items="getFilterActions(item, index)" :obj="item" />
</v-menu>
</template>
</v-data-table>
</template>
<template v-else>
<v-textarea
v-model="filterSetRawContent"
variant="outlined"
auto-grow
/>
<v-form ref="rawFormRef">
<v-textarea
v-model="filterSetRawContent"
variant="outlined"
auto-grow
:rules="[rules.required]"
:error-messages="rawContentErrors"
/>
</v-form>
<v-btn
color="success"
@click="saveFilterSet"
:loading="saving"
class="mt-2"
>
{{ $gettext('Save') }}
</v-btn>
@@ -131,6 +136,7 @@ import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { useBusStore } from '@/stores'
import rules from '@/plugins/rules'
import accountApi from '@/api/account'
import FilterSetForm from '@/components/account/FilterSetForm.vue'
import FilterForm from '@/components/account/FilterForm.vue'
@@ -146,6 +152,8 @@ const filters = ref([])
const filterSetRawContent = ref('')
const currentFilterSet = ref()
const loadingFilters = ref(false)
const rawContentErrors = ref([])
const rawFormRef = ref()
const rawMode = ref(false)
const saving = ref(false)
const selectedFilter = ref(null)
@@ -181,6 +189,12 @@ watch(rawMode, (value) => {
}
})
watch(filterSetRawContent, () => {
if (rawContentErrors.value.length) {
rawContentErrors.value = []
}
})
function getFilterSetName(filterSet) {
let result = filterSet.name
if (filterSet.active) {
@@ -248,6 +262,16 @@ function deleteFilter(filter) {
})
}
async function moveFilterDown(filter) {
await accountApi.moveFilterDown(currentFilterSet.value.name, filter.name)
fetchFilters(currentFilterSet.value.name)
}
async function moveFilterUp(filter) {
await accountApi.moveFilterUp(currentFilterSet.value.name, filter.name)
fetchFilters(currentFilterSet.value.name)
}
async function activateFilterSet(filterSet) {
await accountApi.activateFilterSet(filterSet.name)
busStore.displayNotification({ msg: $gettext('Filter set activated') })
@@ -261,11 +285,22 @@ async function deleteFilterSet(filterSet) {
}
async function saveFilterSet() {
const { valid } = await rawFormRef.value.validate()
if (!valid) {
return
}
const data = { content: filterSetRawContent.value }
saving.value = true
await accountApi.saveFilterSet(currentFilterSet.value.name, data)
saving.value = false
busStore.displayNotification({ msg: $gettext('Filter set updated') })
try {
await accountApi.saveFilterSet(currentFilterSet.value.name, data)
busStore.displayNotification({ msg: $gettext('Filter set updated') })
} catch (err) {
if (err.response.status === 400 && err.response.data.content) {
rawContentErrors.value = err.response.data.content
}
} finally {
saving.value = false
}
}
async function downloadFilterSet(filterSet) {
@@ -292,7 +327,7 @@ const filterActions = [
}
]
function getFilterActions(filter) {
function getFilterActions(filter, index) {
const result = [...filterActions]
if (filter.enabled) {
result.push({
@@ -309,6 +344,20 @@ function getFilterActions(filter) {
onClick: enableFilter
})
}
if (index !== filters.value.length - 1) {
result.push({
label: $gettext('Move down'),
icon: 'mdi-arrow-down',
onClick: moveFilterDown
})
}
if (index !== 0) {
result.push({
label: $gettext('Move up'),
icon: 'mdi-arrow-up',
onClick: moveFilterUp
})
}
return result
}
@@ -341,12 +390,3 @@ function getFilterSetActions() {
fetchFilterSets()
</script>
<style scoped>
/* .v-toolbar {
background-color: white !important;
} */
.v-tabs {
background-color: #f7f8fa !important;
}
</style>
@@ -5,6 +5,8 @@ from typing import List, Tuple
from sievelib import commands
from sievelib.factory import Filter
from django.utils.translation import gettext as _
from rest_framework import serializers
from modoboa.sievefilters import constants, lib
@@ -20,6 +22,13 @@ class FilterSetContentSerializer(serializers.Serializer):
content = serializers.CharField()
def validate_content(self, value: str):
sclient = self.context["sclient"]
if not sclient.msc.checkscript(value):
error = sclient.msc.errmsg.decode().strip().split("\r\n")[0]
raise serializers.ValidationError(error)
return value
class ConditionSerializer(serializers.Serializer):
+60 -15
View File
@@ -7,6 +7,7 @@ from sievelib.commands import BadArgument, BadValue
from sievelib.factory import FilterAlreadyExists
from django.http import HttpResponse
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
@@ -20,6 +21,14 @@ from modoboa.sievefilters.rfc6266 import build_header
from modoboa.sievefilters.api.v2 import serializers
FILTER_SET_ID = OpenApiParameter(
"id",
OpenApiTypes.STR,
description="Name of an existing filter set",
location=OpenApiParameter.PATH,
)
class FilterSetViewSet(viewsets.ViewSet):
serializer_class = serializers.FilterSetSerializer
@@ -62,12 +71,15 @@ class FilterSetViewSet(viewsets.ViewSet):
fset = sclient.getscript(pk)
except SieveClientError:
return response.Response(status=404)
serializer = serializers.FilterSetContentSerializer(data=request.data)
serializer = serializers.FilterSetContentSerializer(
data=request.data, context={"sclient": sclient}
)
serializer.is_valid(raise_exception=True)
try:
sclient.pushscript(pk, serializer.validated_data["content"])
except SieveClientError as e:
error = str(e)
return response.Response({"content": error}, status=400)
return response.Response(serializer.validated_data)
@action(methods=["post"], detail=True)
@@ -135,22 +147,33 @@ class FilterSetViewSet(viewsets.ViewSet):
return response.Response(serializer.data)
@extend_schema(
parameters=[
OpenApiParameter("id", OpenApiTypes.STR),
],
request=serializers.FilterSerializer,
responses={201: serializers.FilterSerializer},
parameters=[FILTER_SET_ID],
responses={200: serializers.FilterSerializer(many=True)},
)
@action(methods=["get", "post"], detail=True)
def filters(self, request, pk=None):
@action(methods=["get"], detail=True, url_path="filters")
def get_filters(self, request, pk=None):
"""Retrieve a list of all filters associated to given filter set."""
sclient = self.get_sieve_client(request)
try:
fset = sclient.getscript(pk, format="fset")
except SieveClientError:
return response.Response(status=404)
serializer = serializers.FilterSerializer.from_filters(fset.filters)
return response.Response(serializer.data)
@extend_schema(
parameters=[FILTER_SET_ID],
request=serializers.FilterSerializer,
responses={201: serializers.FilterSerializer},
)
@get_filters.mapping.post
def filters(self, request, pk=None):
"""Create a new filter."""
sclient = self.get_sieve_client(request)
try:
fset = sclient.getscript(pk, format="fset")
except SieveClientError:
return response.Response(status=404)
if request.method == "GET":
serializer = serializers.FilterSerializer.from_filters(fset.filters)
return response.Response(serializer.data)
serializer = serializers.FilterSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
match_type, conditions, actions = serializer.to_filter()
@@ -160,8 +183,8 @@ class FilterSetViewSet(viewsets.ViewSet):
except (BadArgument, BadValue) as inst:
return response.Response(str(inst), status=400)
except FilterAlreadyExists:
return response.Response(
{"error": f"Filter {fset.name} already exists"}, status=409
raise serializers.ValidationError(
{"name": _("Filter %s already exists") % fltname}
)
sclient.pushscript(fset.name, str(fset))
return response.Response(serializer.validated_data, status=201)
@@ -169,7 +192,7 @@ class FilterSetViewSet(viewsets.ViewSet):
@extend_schema(
description="Update an existing filter",
parameters=[
OpenApiParameter("id", OpenApiTypes.STR, location=OpenApiParameter.PATH),
FILTER_SET_ID,
OpenApiParameter(
"filter", OpenApiTypes.STR, location=OpenApiParameter.PATH
),
@@ -226,7 +249,7 @@ class FilterSetViewSet(viewsets.ViewSet):
@extend_schema(
description="Delete an existing filter",
parameters=[
OpenApiParameter("id", OpenApiTypes.STR, location=OpenApiParameter.PATH),
FILTER_SET_ID,
OpenApiParameter(
"filter", OpenApiTypes.STR, location=OpenApiParameter.PATH
),
@@ -243,3 +266,25 @@ class FilterSetViewSet(viewsets.ViewSet):
sclient.pushscript(fset.name, str(fset))
return response.Response(status=204)
return response.Response(status=404)
def move_filter(self, request, filterset: str, filter: str, direction: str):
sclient = self.get_sieve_client(request)
try:
fset = sclient.getscript(filterset, format="fset")
except SieveClientError:
return response.Response(status=404)
fset.movefilter(filter, direction)
sclient.pushscript(filterset, str(fset))
return response.Response(status=204)
@action(
methods=["post"], detail=True, url_path="filters/(?P<filter>[^/.]+)/move_up"
)
def move_filter_up(self, request, pk: str, filter: str):
return self.move_filter(request, pk, filter, "up")
@action(
methods=["post"], detail=True, url_path="filters/(?P<filter>[^/.]+)/move_down"
)
def move_filter_down(self, request, pk: str, filter: str):
return self.move_filter(request, pk, filter, "down")