mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-20 06:08:35 +02:00
Merge pull request #2485 from modoboa/feature/limits
Manage domain and account resources.
This commit is contained in:
@@ -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')
|
||||
},
|
||||
|
||||
+2
-2
@@ -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>
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user