mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Improved raw mode and added new filter ops
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user