mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Introduced API versioning.
This commit is contained in:
@@ -1 +1 @@
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000/api/v1
|
||||
VUE_APP_API_BASE_URL=http://localhost:8000/api/v2
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<span class="headline">{{ title }}</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="form.name" label="Name" />
|
||||
<v-select
|
||||
v-model="form.target"
|
||||
:items="domains"
|
||||
item-text="name"
|
||||
item-value="pk"
|
||||
label="Choose a domain"
|
||||
single-line
|
||||
></v-select>
|
||||
<v-switch
|
||||
v-model="form.enabled"
|
||||
label="Enabled"
|
||||
color="primary"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="gray darken-1"
|
||||
text
|
||||
@click="close"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary darken-1"
|
||||
text
|
||||
@click="submit"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
domainAlias: Object
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'domains/domains'
|
||||
}),
|
||||
submitLabel () {
|
||||
return (this.domainAlias) ? 'Update' : 'Add'
|
||||
},
|
||||
title () {
|
||||
return (this.domainAlias) ? 'Edit domain alias' : 'Add a new domain alias'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
form: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('close')
|
||||
this.form = {}
|
||||
},
|
||||
submit () {
|
||||
if (!this.form.pk) {
|
||||
this.$store.dispatch('domains/addDomainAlias', this.form).then(resp => {
|
||||
this.close()
|
||||
})
|
||||
} else {
|
||||
const data = JSON.parse(JSON.stringify(this.form))
|
||||
data.target = data.target.pk
|
||||
this.$store.dispatch('domains/updateDomainAlias', { id: this.form.pk, data: data }).then(resp => {
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.domainAlias) {
|
||||
this.form = JSON.parse(JSON.stringify(this.domainAlias))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
domainAlias (value) {
|
||||
if (value) {
|
||||
this.form = JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -12,13 +12,16 @@
|
||||
<v-btn class="mr-2" fab small>
|
||||
<v-icon>mdi-file-import-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn class="mr-2" fab small>
|
||||
<v-btn class="mr-2" fab small @click="exportList" title="Export data">
|
||||
<v-icon>mdi-file-export-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab small color="primary" @click="showForm = true">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-data-table :headers="headers" :items="domainAliases" :search="search" class="elevation-1">
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn icon small :to="{ name: 'DomainEdit', params: { domainPk: item.pk }}">
|
||||
<v-btn icon small title="Edit this domain alias" @click="editDomainAlias(item)">
|
||||
<v-icon>mdi-circle-edit-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon small title="Remove" @click="confirmDelete(item)">
|
||||
@@ -26,9 +29,7 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.name="{ item }">
|
||||
<router-link :to="{ name: 'DomainDetail', params: { pk: item.pk } }">
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
{{ item.name }}
|
||||
</template>
|
||||
<template v-slot:item.dns_status="{ }">
|
||||
</template>
|
||||
@@ -36,17 +37,25 @@
|
||||
|
||||
<confirm-dialog v-model="showConfirmDialog"
|
||||
:message="deleteDomainMsg"
|
||||
@confirm="deleteDomain" />
|
||||
@confirm="deleteDomainAlias" />
|
||||
|
||||
<v-dialog v-model="showForm"
|
||||
persistent
|
||||
max-width="800px">
|
||||
<domain-alias-form @close="closeForm" :domain-alias="selectedDomainAlias" />
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog'
|
||||
import DomainAliasForm from '@/components/domains/DomainAliasForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConfirmDialog
|
||||
ConfirmDialog,
|
||||
DomainAliasForm
|
||||
},
|
||||
computed: mapGetters({
|
||||
domainAliases: 'domains/domainAliases'
|
||||
@@ -56,24 +65,34 @@ export default {
|
||||
headers: [
|
||||
{ text: '', value: 'actions', sortable: false },
|
||||
{ text: 'Name', value: 'name' },
|
||||
{ text: 'Target', value: 'target.name' },
|
||||
{ text: 'DNS status', value: 'dns_status', sortable: false }
|
||||
],
|
||||
deleteDomainMsg: 'Confirm deletion?',
|
||||
selectedDomain: null,
|
||||
showConfirmDialog: false,
|
||||
search: ''
|
||||
showForm: false,
|
||||
search: '',
|
||||
selectedDomainAlias: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('domains/getDomainAliases')
|
||||
},
|
||||
methods: {
|
||||
confirmDelete (domain) {
|
||||
this.selectedDomain = domain
|
||||
closeForm () {
|
||||
this.showForm = false
|
||||
this.selectedDomainAlias = null
|
||||
},
|
||||
editDomainAlias (domainAlias) {
|
||||
this.selectedDomainAlias = domainAlias
|
||||
this.showForm = true
|
||||
},
|
||||
confirmDelete (domainAlias) {
|
||||
this.selectedDomainAlias = domainAlias
|
||||
this.showConfirmDialog = true
|
||||
},
|
||||
deleteDomain () {
|
||||
|
||||
deleteDomainAlias () {
|
||||
this.$store.dispatch('domains/deleteDomainAlias', this.selectedDomainAlias.pk)
|
||||
},
|
||||
getDNSTagType (value) {
|
||||
if (value === 'unknown') {
|
||||
@@ -83,6 +102,21 @@ export default {
|
||||
return 'warning'
|
||||
}
|
||||
return 'success'
|
||||
},
|
||||
exportList () {
|
||||
this.$axios.get('/domainaliases/', { headers: { Accept: 'text/csv' } }).then(resp => {
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([resp.data], { type: 'text/csv' }))
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.style.display = 'none'
|
||||
link.href = url
|
||||
link.setAttribute('download', 'domain_aliases.csv')
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ _axios.interceptors.response.use(
|
||||
return Promise.reject(error)
|
||||
}
|
||||
return _axios.post('/token/refresh/', { refresh: refreshToken }).then(resp => {
|
||||
console.log('OK')
|
||||
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
|
||||
_axios.defaults.headers.common.Authorization = `Bearer ${resp.data.access}`
|
||||
const config = error.config
|
||||
|
||||
@@ -5,6 +5,9 @@ const SET_DOMAINS = 'SET_DOMAINS'
|
||||
const ADD_DOMAIN = 'ADD_DOMAIN'
|
||||
const UPDATE_DOMAIN = 'UPDATE_DOMAIN'
|
||||
const SET_DOMAIN_ALIASES = 'SET_DOMAIN_ALIASES'
|
||||
const ADD_DOMAIN_ALIAS = 'ADD_DOMAIN_ALIAS'
|
||||
const UPDATE_DOMAIN_ALIAS = 'UPDATE_DOMAIN_ALIAS'
|
||||
const REMOVE_DOMAIN_ALIAS = 'REMOVE_DOMAIN_ALIAS'
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
@@ -45,9 +48,24 @@ const actions = {
|
||||
})
|
||||
},
|
||||
getDomainAliases ({ commit }) {
|
||||
return Vue.prototype.$axios.get('/domainaliases/').then(response => {
|
||||
return Vue.prototype.$axios.get('/domainaliases/?expand=target').then(response => {
|
||||
commit(SET_DOMAIN_ALIASES, response.data)
|
||||
})
|
||||
},
|
||||
addDomainAlias ({ commit }, data) {
|
||||
return Vue.prototype.$axios.post('/domainaliases/', data).then(response => {
|
||||
commit(ADD_DOMAIN_ALIAS, response.data)
|
||||
})
|
||||
},
|
||||
updateDomainAlias ({ commit }, { id, data }) {
|
||||
return Vue.prototype.$axios.put(`/domainaliases/${id}/`, data).then(response => {
|
||||
commit(UPDATE_DOMAIN_ALIAS, response.data)
|
||||
})
|
||||
},
|
||||
deleteDomainAlias ({ commit }, domainAliasId) {
|
||||
return Vue.prototype.$axios.delete(`/domainaliases/${domainAliasId}/`).then(response => {
|
||||
commit(REMOVE_DOMAIN_ALIAS, domainAliasId)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +87,19 @@ const mutations = {
|
||||
},
|
||||
[SET_DOMAIN_ALIASES] (state, domainAliases) {
|
||||
state.domainAliases = domainAliases
|
||||
},
|
||||
[ADD_DOMAIN_ALIAS] (state, domainAlias) {
|
||||
state.domainAliases.push(domainAlias)
|
||||
},
|
||||
[UPDATE_DOMAIN_ALIAS] (state, domainAlias) {
|
||||
state.domainAliases.filter((item, pos) => {
|
||||
if (item.pk === domainAlias.pk) {
|
||||
Vue.set(state.domainAliases, pos, domainAlias)
|
||||
}
|
||||
})
|
||||
},
|
||||
[REMOVE_DOMAIN_ALIAS] (state, domainAliasId) {
|
||||
state.domainAliases = state.domainAliases.filter(item => item.pk !== domainAliasId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-8
@@ -4,7 +4,8 @@ from django import http
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from rest_framework import filters, status, viewsets
|
||||
from django_filters import rest_framework as dj_filters
|
||||
from rest_framework import filters, renderers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
|
||||
@@ -12,6 +13,8 @@ from rest_framework.response import Response
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
from modoboa.core import sms_backends
|
||||
from modoboa.lib import renderers as lib_renderers
|
||||
from modoboa.lib import viewsets as lib_viewsets
|
||||
|
||||
from . import lib, models, serializers
|
||||
|
||||
@@ -40,20 +43,34 @@ class DomainViewSet(viewsets.ModelViewSet):
|
||||
instance.delete(self.request.user)
|
||||
|
||||
|
||||
class DomainAliasViewSet(viewsets.ModelViewSet):
|
||||
class DomainAliasFilterSet(dj_filters.FilterSet):
|
||||
"""Custom FilterSet for DomainAlias."""
|
||||
|
||||
domain = dj_filters.CharFilter(field_name="target__name")
|
||||
|
||||
class Meta:
|
||||
model = models.DomainAlias
|
||||
fields = ["domain"]
|
||||
|
||||
|
||||
class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet):
|
||||
"""ViewSet for DomainAlias."""
|
||||
|
||||
filter_backends = (dj_filters.DjangoFilterBackend, )
|
||||
filterset_class = DomainAliasFilterSet
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
|
||||
renderer_classes = (renderers.JSONRenderer, lib_renderers.CSVRenderer)
|
||||
serializer_expanded_fields = ["target"]
|
||||
serializer_class = serializers.DomainAliasSerializer
|
||||
http_method_names = ["get", "post", "put", "delete"]
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter queryset based on current user."""
|
||||
queryset = models.DomainAlias.objects.get_for_admin(self.request.user)
|
||||
domain = self.request.query_params.get("domain")
|
||||
if domain:
|
||||
queryset = queryset.filter(target__name=domain)
|
||||
return queryset
|
||||
return models.DomainAlias.objects.get_for_admin(self.request.user)
|
||||
|
||||
def get_renderer_context(self):
|
||||
context = super().get_renderer_context()
|
||||
context["headers"] = ["name", "target__name", "enabled"]
|
||||
return context
|
||||
|
||||
|
||||
class AccountViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from rest_flex_fields import FlexFieldsModelSerializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
@@ -105,12 +106,15 @@ class DomainSerializer(serializers.ModelSerializer):
|
||||
return domain
|
||||
|
||||
|
||||
class DomainAliasSerializer(serializers.ModelSerializer):
|
||||
class DomainAliasSerializer(FlexFieldsModelSerializer):
|
||||
"""Base DomainAlias serializer."""
|
||||
|
||||
class Meta:
|
||||
model = admin_models.DomainAlias
|
||||
fields = ("pk", "name", "target", "enabled", )
|
||||
expandable_fields = {
|
||||
"target": DomainSerializer
|
||||
}
|
||||
|
||||
def validate_target(self, value):
|
||||
"""Check target domain."""
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Custom renderers for DRF."""
|
||||
|
||||
import csv
|
||||
from io import StringIO
|
||||
|
||||
from rest_framework import renderers
|
||||
|
||||
|
||||
class CSVRenderer(renderers.BaseRenderer):
|
||||
"""Custom CSV renderer."""
|
||||
|
||||
media_type = "text/csv"
|
||||
format = "csv"
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
if renderer_context is None:
|
||||
renderer_context = {}
|
||||
headers = renderer_context.get("headers")
|
||||
with StringIO() as fp:
|
||||
csvwriter = csv.writer(fp)
|
||||
for item in data:
|
||||
if headers is None:
|
||||
headers = item.keys()
|
||||
row = ["domainalias"]
|
||||
for header in headers:
|
||||
if "__" in header:
|
||||
parts = header.split("__")
|
||||
value = item
|
||||
for part in parts:
|
||||
value = value.get(part)
|
||||
else:
|
||||
value = item.get(header)
|
||||
row.append(value)
|
||||
csvwriter.writerow(row)
|
||||
content = fp.getvalue()
|
||||
return content
|
||||
@@ -0,0 +1,34 @@
|
||||
"""ViewSet related mixins and tools."""
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
|
||||
class ExpandableSerializerMixin:
|
||||
"""
|
||||
A mixin to allow serializers with expanded fields by default.
|
||||
|
||||
Useful for create/update operations that return a different output.
|
||||
"""
|
||||
|
||||
serializer_expanded_fields = []
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
"""
|
||||
Return the serializer instance that should be used for validating and
|
||||
deserializing input, and for serializing output.
|
||||
"""
|
||||
serializer_class = self.get_serializer_class()
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
if self.request.version == 'v2':
|
||||
kwargs['expand'] = self.serializer_expanded_fields
|
||||
return serializer_class(*args, **kwargs)
|
||||
|
||||
|
||||
class ExpandableModelViewSet(ExpandableSerializerMixin,
|
||||
viewsets.ModelViewSet):
|
||||
"""
|
||||
A ModelViewSet version which provides an expanded serializer
|
||||
output for create/update actions.
|
||||
"""
|
||||
|
||||
pass
|
||||
+2
-1
@@ -70,7 +70,8 @@ urlpatterns += [
|
||||
path('docs/api/', login_required(
|
||||
TemplateView.as_view(template_name="swagger-ui.html")),
|
||||
name="docs-index"),
|
||||
path('api/v1/', include("modoboa.urls_api")),
|
||||
path('api/v1/', include("modoboa.urls_api", namespace="v1")),
|
||||
path('api/v2/', include("modoboa.urls_api", namespace="v2")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
Reference in New Issue
Block a user