Introduced API versioning.

This commit is contained in:
Antoine Nguyen
2020-10-30 18:49:30 +01:00
parent 52cc076735
commit b9f233c126
10 changed files with 281 additions and 26 deletions
+1 -1
View File
@@ -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)
})
}
}
}
-1
View File
@@ -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
+32 -1
View File
@@ -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
View File
@@ -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):
+5 -1
View File
@@ -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."""
+36
View File
@@ -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
+34
View File
@@ -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
View File
@@ -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: