Merge pull request #2485 from modoboa/feature/limits

Manage domain and account resources.
This commit is contained in:
Antoine Nguyen
2022-04-07 14:34:36 +02:00
committed by GitHub
8 changed files with 170 additions and 25 deletions
+41 -4
View File
@@ -133,6 +133,18 @@
<domain-transport-form ref="transportForm" :domain="editedDomain" />
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel v-if="limitsConfig.enable_domain_limits && (authUser.role === 'SuperAdmins' || authUser.role === 'Resellers')">
<v-expansion-panel-header>
<v-row no-gutters>
<v-col cols="4">
<translate>Resources</translate>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<resources-form ref="resourcesForm" :resources="domain.resources" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn color="grey lighten-1" @click="$router.go(-1)">
@@ -151,18 +163,28 @@ import DomainDNSForm from './DomainDNSForm'
import DomainGeneralForm from './DomainGeneralForm'
import DomainLimitationsForm from './DomainLimitationsForm'
import DomainTransportForm from './DomainTransportForm'
import { mapGetters } from 'vuex'
import parameters from '@/api/parameters'
import ResourcesForm from '@/components/tools/ResourcesForm'
export default {
components: {
'domain-dns-form': DomainDNSForm,
DomainGeneralForm,
DomainLimitationsForm,
DomainTransportForm
DomainTransportForm,
ResourcesForm
},
props: ['domain'],
computed: {
...mapGetters({
authUser: 'auth/authUser'
})
},
data () {
return {
editedDomain: {},
limitsConfig: {},
panel: 0
}
},
@@ -174,14 +196,29 @@ export default {
return
}
}
if (this.editedDomain.type === 'relaydomain') {
this.$refs.transportForm.checkSettingTypes(this.editedDomain)
if (this.$refs.resourcesForm !== undefined) {
const valid = await this.$refs.resourcesForm.validateForm()
if (!valid) {
return
}
}
this.$store.dispatch('domains/updateDomain', this.editedDomain).then(resp => {
const data = { ...this.editedDomain }
if (data.transport === null) {
delete data.transport
}
if (data.type === 'relaydomain') {
this.$refs.transportForm.checkSettingTypes(data)
}
this.$store.dispatch('domains/updateDomain', data).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Domain updated') })
})
}
},
created () {
parameters.getApplication('limits').then(resp => {
this.limitsConfig = resp.data
})
},
watch: {
domain: {
handler: function (val) {
@@ -98,16 +98,16 @@
<account-mailbox-form ref="mailboxForm" :account="editedAccount" />
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel v-if="account.role !== 'SimpleUsers'">
<v-expansion-panel v-if="limitsConfig.enable_admin_limits && account.role !== 'SimpleUsers'">
<v-expansion-panel-header>
<v-row no-gutters>
<v-col cols="4">
<translate>Limits</translate>
<translate>Resources</translate>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<account-resources-form ref="resourcesForm" :account="editedAccount" />
<resources-form ref="resourcesForm" :resources="editedAccount.resources" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
@@ -128,15 +128,16 @@ import { bus } from '@/main'
import accounts from '@/api/accounts'
import AccountGeneralForm from './AccountGeneralForm'
import AccountMailboxForm from './AccountMailboxForm'
import AccountResourcesForm from './AccountResourcesForm'
import AccountRoleForm from './AccountRoleForm'
import ResourcesForm from '@/components/tools/ResourcesForm'
import parameters from '@/api/parameters'
export default {
components: {
AccountGeneralForm,
AccountMailboxForm,
AccountResourcesForm,
AccountRoleForm
AccountRoleForm,
ResourcesForm
},
props: ['account'],
computed: {
@@ -147,6 +148,7 @@ export default {
data () {
return {
editedAccount: {},
limitsConfig: {},
panel: 0
}
},
@@ -197,6 +199,11 @@ export default {
}
}
},
created () {
parameters.getApplication('limits').then(resp => {
this.limitsConfig = resp.data
})
},
mounted () {
this.$store.dispatch('identities/fetchDomains')
},
@@ -5,7 +5,7 @@
</v-card-title>
<v-card-text>
<div
v-for="resource in account.resources"
v-for="resource in resources"
:key="resource.name"
class="mt-4"
>
@@ -22,7 +22,7 @@
<script>
export default {
props: {
account: Object
resources: Array
},
methods: {
getProgressColor (value) {
@@ -0,0 +1,37 @@
<template>
<validation-observer ref="observer">
<validation-provider
v-for="resource in resources"
:key="resource.name"
v-slot="{ errors }"
rules="required"
>
<label class="m-label">{{ resource.label }}</label>
<v-text-field
v-model="resource.max_value"
type="number"
outlined
dense
:error-messages="errors"
/>
</validation-provider>
</validation-observer>
</template>
<script>
export default {
props: {
resources: Array
},
methods: {
async validateForm () {
return await this.$refs.observer.validate()
},
getPayload () {
return this.resources.map(r => {
return { name: r.name, max_value: r.max_value }
})
}
}
}
</script>
+9
View File
@@ -17,6 +17,8 @@
</v-col>
<v-col cols="6">
<dns-detail :domain="domain" />
<div class="mt-4" />
<resources v-if="limitsConfig.enable_domain_limits && domain.resources && domain.resources.length" :resources="domain.resources" />
</v-col>
</v-row>
</v-layout>
@@ -45,6 +47,8 @@ import AdministratorList from '@/components/domains/AdministratorList'
import DNSDetail from '@/components/domains/DNSDetail'
import DomainForm from '@/components/domains/DomainForm'
import DomainSummary from '@/components/domains/DomainSummary'
import parameters from '@/api/parameters'
import Resources from '@/components/tools/Resources'
import TimeSerieChart from '@/components/tools/TimeSerieChart'
export default {
@@ -53,11 +57,13 @@ export default {
'dns-detail': DNSDetail,
DomainForm,
DomainSummary,
Resources,
TimeSerieChart
},
data () {
return {
domain: { pk: this.$route.params.id },
limitsConfig: {},
showEditForm: false,
showAdminList: false
}
@@ -65,6 +71,9 @@ export default {
methods: {
},
mounted () {
parameters.getApplication('limits').then(resp => {
this.limitsConfig = resp.data
})
domains.getDomain(this.$route.params.id).then(resp => {
this.domain = resp.data
})
+12 -7
View File
@@ -11,9 +11,9 @@
<account-summary :account="account" />
</v-col>
<v-col cols="6">
<account-resources
v-if="account.resources && account.resources.length"
:account="account"
<resources
v-if="limitsConfig.enable_admin_limits && account.resources && account.resources.length"
:resources="account.resources"
/>
<account-aliases v-if="account.aliases" class="mt-2" :account="account" />
</v-col>
@@ -29,26 +29,31 @@
<script>
import accounts from '@/api/accounts'
import AccountAliases from '@/components/identities/AccountAliases'
import AccountResources from '@/components/identities/AccountResources'
import AccountSummary from '@/components/identities/AccountSummary'
import DomainAdminDomains from '@/components/identities/DomainAdminDomains'
import parameters from '@/api/parameters'
import Resources from '@/components/tools/Resources'
export default {
components: {
AccountAliases,
AccountResources,
AccountSummary,
DomainAdminDomains
DomainAdminDomains,
Resources
},
data () {
return {
account: { pk: this.$route.params.id }
account: { pk: this.$route.params.id },
limitsConfig: {}
}
},
mounted () {
accounts.get(this.$route.params.id).then(resp => {
this.account = resp.data
})
parameters.getApplication('limits').then(resp => {
this.limitsConfig = resp.data
})
}
}
</script>
+40 -6
View File
@@ -35,6 +35,14 @@ class CreateDomainAdminSerializer(serializers.Serializer):
with_aliases = serializers.BooleanField(default=False)
class DomainResourceSerializer(serializers.ModelSerializer):
"""Serializer for domain resource."""
class Meta:
fields = ("name", "label", "max_value", "current_value", "usage")
model = limits_models.DomainObjectLimit
class DomainSerializer(v1_serializers.DomainSerializer):
"""Domain serializer for v2 API."""
@@ -46,6 +54,14 @@ class DomainSerializer(v1_serializers.DomainSerializer):
"domain_admin", "transport"
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not param_tools.get_global_parameter("enable_domain_limits", app="limits"):
return
self.fields["resources"] = DomainResourceSerializer(
many=True, source="domainobjectlimit_set", required=False
)
def validate(self, data):
result = super().validate(data)
dtype = data.get("type", "domain")
@@ -122,6 +138,7 @@ class DomainSerializer(v1_serializers.DomainSerializer):
def update(self, instance, validated_data):
"""Update domain and create/update transport."""
transport_def = validated_data.pop("transport", None)
resources = validated_data.pop("domainobjectlimit_set", None)
super().update(instance, validated_data)
if transport_def and instance.type == "relaydomain":
transport = getattr(instance, "transport", None)
@@ -138,6 +155,11 @@ class DomainSerializer(v1_serializers.DomainSerializer):
if created:
instance.transport = transport
instance.save()
if resources:
for resource in resources:
instance.domainobjectlimit_set.filter(
name=resource["name"]
).update(max_value=resource["max_value"])
return instance
@@ -253,7 +275,7 @@ class IdentitySerializer(serializers.Serializer):
tags = TagSerializer(many=True)
class ResourceSerializer(serializers.ModelSerializer):
class AccountResourceSerializer(serializers.ModelSerializer):
"""Serializer for user resource."""
class Meta:
@@ -268,15 +290,22 @@ class AccountSerializer(v1_serializers.AccountSerializer):
child=lib_fields.DRFEmailFieldUTF8(),
source="mailbox.alias_addresses"
)
resources = serializers.SerializerMethodField()
class Meta(v1_serializers.AccountSerializer.Meta):
fields = (
v1_serializers.AccountSerializer.Meta.fields
+ ("aliases", "resources", )
+ ("aliases", )
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not param_tools.get_global_parameter("enable_admin_limits", app="limits"):
return
self.fields["resources"] = serializers.SerializerMethodField()
def get_resources(self, account):
if account.role == 'SimpleUsers':
return []
resources = []
for limit in account.userobjectlimit_set.all():
tpl = limits_constants.DEFAULT_USER_LIMITS[limit.name]
@@ -284,7 +313,7 @@ class AccountSerializer(v1_serializers.AccountSerializer):
if account.role != tpl["required_role"]:
continue
resources.append(limit)
return ResourceSerializer(resources, many=True).data
return AccountResourceSerializer(resources, many=True).data
class MailboxSerializer(serializers.ModelSerializer):
@@ -329,14 +358,19 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
required=False
)
mailbox = MailboxSerializer(required=False)
resources = WritableResourceSerializer(many=True, required=False)
class Meta(v1_serializers.WritableAccountSerializer.Meta):
fields = tuple(
field
for field in v1_serializers.WritableAccountSerializer.Meta.fields
if field != "random_password"
) + ("aliases", "resources")
) + ("aliases",)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not param_tools.get_global_parameter("enable_admin_limits", app="limits"):
return
self.fields["resources"] = WritableResourceSerializer(many=True, required=False)
def validate_aliases(self, value):
"""Check if required domains are locals and user can access them."""
+16
View File
@@ -68,6 +68,22 @@ class DomainViewSetTestCase(ModoAPITestCase):
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
def test_update_resources(self):
self.set_global_parameter("enable_domain_limits", True, app="limits")
domain = models.Domain.objects.get(name="test2.com")
data = {
"name": "test2.com",
"resources": [
{"name": "domain_aliases", "max_value": 20}
]
}
url = reverse("v2:domain-detail", args=[domain.pk])
resp = self.client.put(url, data, format="json")
self.assertEqual(resp.status_code, 200)
self.assertEqual(
domain.domainobjectlimit_set.get(name="domain_aliases").max_value, 20
)
def test_delete(self):
self.client.credentials(
HTTP_AUTHORIZATION="Token " + self.da_token.key)