-
{{ $gettext('Add') }}
@@ -73,7 +73,7 @@
{{ $yesno(item.enabled) }}
-
+
-
+
-
+
+
+
{{ $gettext('Save') }}
@@ -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()
-
-
diff --git a/modoboa/sievefilters/api/v2/serializers.py b/modoboa/sievefilters/api/v2/serializers.py
index dd9eec624..f492959d1 100644
--- a/modoboa/sievefilters/api/v2/serializers.py
+++ b/modoboa/sievefilters/api/v2/serializers.py
@@ -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):
diff --git a/modoboa/sievefilters/api/v2/viewsets.py b/modoboa/sievefilters/api/v2/viewsets.py
index 6dbba9853..e3cdc60ae 100644
--- a/modoboa/sievefilters/api/v2/viewsets.py
+++ b/modoboa/sievefilters/api/v2/viewsets.py
@@ -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[^/.]+)/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[^/.]+)/move_down"
+ )
+ def move_filter_down(self, request, pk: str, filter: str):
+ return self.move_filter(request, pk, filter, "down")