mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
680 lines
22 KiB
680 lines
22 KiB
<template> |
|
<v-card class="pb-2"> |
|
<v-toolbar flat height="42" class="toolbar-border-bottom"> |
|
<v-spacer /> |
|
<x-btn |
|
outlined |
|
tooltip="Validation Documentation" |
|
color="primary" |
|
small |
|
href="https://docs.nocodb.com/en/v0.5/database/database-model-validation" |
|
target="_blank" |
|
> |
|
<v-icon small left> mdi-book-open-variant </v-icon> |
|
Validation Docs |
|
</x-btn> |
|
<x-btn outlined tooltip="Reload validation" color="primary" small @click="loadTableModelMeta"> |
|
<v-icon small left> refresh </v-icon> |
|
<!-- Reload --> |
|
{{ $t('general.reload') }} |
|
</x-btn> |
|
<x-btn |
|
outlined |
|
:tooltip="$t('tooltip.saveChanges')" |
|
color="primary" |
|
small |
|
:disabled="!edited || loading" |
|
@click.prevent="saveValidations" |
|
> |
|
<v-icon small left> save </v-icon> |
|
<!-- Save --> |
|
{{ $t('general.save') }} |
|
</x-btn> |
|
</v-toolbar> |
|
<template v-if="columns"> |
|
<p class="title mt-6 mb-6"> |
|
Table : <span class="text-capitalize">{{ columns.title }}</span |
|
><br /> |
|
<span class="font-weight-thin">Write Validations For Columns</span> |
|
</p> |
|
|
|
<!-- <v-row justify="center"> |
|
<v-col cols="4" class="d-flex align-center"> |
|
<label class="mr-3">Table Alias</label> |
|
<v-text-field @input="edited = true" v-model="columns.title"> |
|
|
|
</v-text-field> |
|
</v-col> |
|
</v-row>--> |
|
|
|
<v-simple-table style="max-width: 800px; margin: 0 auto" class="mb-10" dense> |
|
<template #default> |
|
<thead> |
|
<tr> |
|
<th>Column Name</th> |
|
<th>Alias</th> |
|
<th>Validators</th> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
<tr v-for="(item, i) in columns.columns" :key="i"> |
|
<td>{{ item.column_name }}</td> |
|
<td> |
|
<v-edit-dialog lazy> |
|
<span> {{ item.title }}</span> |
|
<template #input> |
|
<v-text-field v-model="item.title" :label="$t('general.edit')" single-line @input="edited = true" /> |
|
</template> |
|
</v-edit-dialog> |
|
</td> |
|
<td> |
|
<x-btn |
|
:tooltip="`Edit/Add validation for '${item.column_name}' column`" |
|
color="primary" |
|
style="text-transform: capitalize" |
|
x-small |
|
outlined |
|
@click="editOrAddValidation(item)" |
|
> |
|
<v-icon class="mr-1" x-small> mdi-lead-pencil </v-icon> |
|
Validation ({{ item.validate.func.length }}) |
|
</x-btn> |
|
</td> |
|
</tr> |
|
</tbody> |
|
</template> |
|
</v-simple-table> |
|
|
|
<!-- <v-expansion-panels accordion style="width: 70%; min-width: 500px; margin:5px auto">--> |
|
<!-- <v-expansion-panel--> |
|
<!-- v-for="(item,i) in columns.columns"--> |
|
<!-- :key="i"--> |
|
<!-- >--> |
|
<!-- <v-expansion-panel-header>{{ item.column_name }} <span--> |
|
<!-- class="caption grey--text ml-2">({{ item.validate.func.length }} validations)</span>--> |
|
<!-- </v-expansion-panel-header>--> |
|
<!-- <v-expansion-panel-content>--> |
|
|
|
<!-- <v-toolbar class="elevation-0" flat height="42">--> |
|
<!-- <v-spacer>--> |
|
<!-- </v-spacer>--> |
|
|
|
<!-- <x-btn outlined tooltip="Add new validation"--> |
|
<!-- color="primary"--> |
|
<!-- small--> |
|
<!-- @click.prevent="(item.validate.func.push(''),edited=true,scrollAndFocusLastRow())">--> |
|
<!-- <v-icon small left>mdi-plus</v-icon>--> |
|
<!-- Add Validation--> |
|
<!-- </x-btn>--> |
|
<!-- </v-toolbar>--> |
|
|
|
<!-- <v-simple-table v-if="item.validate.func && item.validate.func.length" dense>--> |
|
<!-- <thead>--> |
|
<!-- <tr>--> |
|
<!-- <th class="caption font-weight-normal">Validation Function</th>--> |
|
<!-- <th class="caption font-weight-normal">Error Message</th>--> |
|
<!-- <!– <th class="caption font-weight-normal">Optional Argument</th>–>--> |
|
<!-- <th></th>--> |
|
<!-- </tr>--> |
|
<!-- </thead>--> |
|
<!-- <tbody>--> |
|
<!-- <tr v-for="(func,i) in item.validate.func">--> |
|
<!-- <td>--> |
|
<!-- <v-edit-dialog--> |
|
<!-- lazy--> |
|
<!-- >--> |
|
<!-- <span>{{ func }}</span>--> |
|
<!-- <template v-slot:input>--> |
|
|
|
<!-- <v-autocomplete--> |
|
<!-- @change="onFunctionChange(i,item)"--> |
|
<!-- dense--> |
|
<!-- v-model="item.validate.func[i]"--> |
|
<!-- :items="fnList"--> |
|
<!-- item-text="func"--> |
|
<!-- ></v-autocomplete>--> |
|
<!-- </template>--> |
|
<!-- </v-edit-dialog>--> |
|
<!-- </td>--> |
|
<!-- <td>--> |
|
<!-- <v-edit-dialog lazy>--> |
|
<!-- <span> {{ item.validate.msg[i] }}</span>--> |
|
<!-- <template v-slot:input>--> |
|
<!-- <v-text-field--> |
|
<!-- @input="edited=true"--> |
|
<!-- v-model="item.validate.msg[i]"--> |
|
<!-- label="Edit"--> |
|
<!-- single-line--> |
|
<!-- ></v-text-field>--> |
|
<!-- </template>--> |
|
<!-- </v-edit-dialog>--> |
|
<!-- </td>--> |
|
<!-- <!– <td>–>--> |
|
<!-- <!– <v-edit-dialog lazy>–>--> |
|
<!-- <!– <span> {{ item.validate.args[i] }}</span>–>--> |
|
<!-- <!– <template v-slot:input>–>--> |
|
<!-- <!– <v-text-field–>--> |
|
<!-- <!– @input="edited=true"–>--> |
|
<!-- <!– v-model="item.validate.args[i]"–>--> |
|
<!-- <!– label="Edit"–>--> |
|
<!-- <!– single-line–>--> |
|
<!-- <!– ></v-text-field>–>--> |
|
<!-- <!– </template>–>--> |
|
<!-- <!– </v-edit-dialog>–>--> |
|
<!-- <!– </td>–>--> |
|
|
|
<!-- <td>--> |
|
<!-- <x-icon--> |
|
<!-- small--> |
|
<!-- color="error" tooltip="Delete role" @click="deleteValidation(item,i)">--> |
|
<!-- mdi-delete-forever--> |
|
<!-- </x-icon>--> |
|
<!-- </td>--> |
|
<!-- </tr>--> |
|
<!-- </tbody>--> |
|
<!-- </v-simple-table>--> |
|
<!-- <div v-else class="d-flex justify-center">--> |
|
<!-- <v-alert dense outlined type="info" color="grey lighten-1" icon="mdi-information-outline" class="caption"--> |
|
<!-- style="width:auto">--> |
|
<!-- No validation for '{{ item.column_name }}'--> |
|
<!-- </v-alert>--> |
|
<!-- </div>--> |
|
<!-- </v-expansion-panel-content>--> |
|
<!-- </v-expansion-panel>--> |
|
<!-- </v-expansion-panels>--> |
|
</template> |
|
|
|
<v-dialog v-model="validatorEditDialog" max-width="700px"> |
|
<v-card v-if="clickedItem"> |
|
<v-card-title class="headline justify-center mb-5"> |
|
Validations for '{{ clickedItem.column_name }}({{ clickedItem.dt }})' |
|
</v-card-title> |
|
<v-card-text> |
|
<div class="d-flex"> |
|
<v-spacer /> |
|
|
|
<v-btn outlined x-small @click="validatorEditDialog = false"> |
|
<!-- Cancel --> |
|
{{ $t('general.cancel') }} |
|
</v-btn> |
|
<x-btn |
|
outlined |
|
tooltip="Add new validation" |
|
color="primary" |
|
x-small |
|
@click.prevent="clickedItem.validate.func.push(''), (edited = true), scrollAndFocusLastRowInModal()" |
|
> |
|
<v-icon small left> mdi-plus </v-icon> |
|
Add Validation |
|
</x-btn> |
|
<v-btn outlined color="primary" x-small @click.prevent="saveValidationForColumn(clickedItem)"> |
|
<!-- Save --> |
|
{{ $t('general.save') }} |
|
</v-btn> |
|
</div> |
|
<v-simple-table v-if="clickedItem.validate.func && clickedItem.validate.func.length" dense> |
|
<thead> |
|
<tr> |
|
<th class="caption font-weight-normal">Validation Function</th> |
|
<th class="caption font-weight-normal">Error Message</th> |
|
<!-- <th class="caption font-weight-normal">Optional Argument</th>--> |
|
<th /> |
|
</tr> |
|
</thead> |
|
<tbody> |
|
<tr v-for="(func, i) in clickedItem.validate.func" :key="i"> |
|
<td> |
|
<v-edit-dialog lazy> |
|
<span>{{ func }}</span> |
|
<template #input> |
|
<v-autocomplete |
|
v-model="clickedItem.validate.func[i]" |
|
dense |
|
:items="fnList" |
|
item-text="func" |
|
@change="onFunctionChange(i, clickedItem)" |
|
/> |
|
</template> |
|
</v-edit-dialog> |
|
</td> |
|
<td> |
|
<v-edit-dialog lazy> |
|
<span> {{ clickedItem.validate.msg[i] }}</span> |
|
<template #input> |
|
<v-text-field |
|
v-model="clickedItem.validate.msg[i]" |
|
:label="$t('general.edit')" |
|
single-line |
|
@input="edited = true" |
|
/> |
|
</template> |
|
</v-edit-dialog> |
|
</td> |
|
<!-- <td>--> |
|
<!-- <v-edit-dialog lazy>--> |
|
<!-- <span> {{ clickedItem.validate.args[i] }}</span>--> |
|
<!-- <template v-slot:input>--> |
|
<!-- <v-text-field--> |
|
<!-- @input="edited=true"--> |
|
<!-- v-model="clickedItem.validate.args[i]"--> |
|
<!-- label="Edit"--> |
|
<!-- single-line--> |
|
<!-- ></v-text-field>--> |
|
<!-- </template>--> |
|
<!-- </v-edit-dialog>--> |
|
<!-- </td>--> |
|
|
|
<td> |
|
<x-icon small color="error" tooltip="Delete role" @click="deleteValidation(clickedItem, i)"> |
|
mdi-delete-forever |
|
</x-icon> |
|
</td> |
|
</tr> |
|
</tbody> |
|
</v-simple-table> |
|
<div v-else class="d-flex justify-center"> |
|
<v-alert |
|
dense |
|
outlined |
|
type="info" |
|
color="grey lighten-1" |
|
icon="mdi-information-outline" |
|
class="caption mt-4" |
|
style="width: auto" |
|
> |
|
No validation for '{{ clickedItem.column_name }}' |
|
</v-alert> |
|
</div> |
|
</v-card-text> |
|
<!-- <v-card-actions>--> |
|
<!-- <v-spacer></v-spacer>--> |
|
<!-- --> |
|
<!-- </v-card-actions>--> |
|
</v-card> |
|
</v-dialog> |
|
</v-card> |
|
</template> |
|
|
|
<script> |
|
const validatorFnList = [ |
|
{ func: 'contains', args: '', msg: 'Error contains' }, |
|
{ |
|
func: 'equals', |
|
args: '', |
|
msg: 'Error equals', |
|
}, |
|
{ |
|
func: 'isAfter', |
|
args: '', |
|
msg: 'Error isAfter', |
|
}, |
|
{ func: 'isAlpha', args: '', msg: 'Error isAlpha' }, |
|
{ |
|
func: 'isAlphanumeric', |
|
args: '', |
|
msg: 'Error isAlphanumeric', |
|
}, |
|
{ |
|
func: 'isAscii', |
|
args: '', |
|
msg: 'Error isAscii', |
|
}, |
|
{ func: 'isBase32', args: '', msg: 'Error isBase32' }, |
|
{ func: 'isBase64', args: '', msg: 'Error isBase64' }, |
|
{ |
|
func: 'isBefore', |
|
args: '', |
|
msg: 'Error isBefore', |
|
}, |
|
{ func: 'isBIC', args: '', msg: 'Error isBIC' }, |
|
{ func: 'isBoolean', args: '', msg: 'Error isBoolean' }, |
|
{ |
|
func: 'isBtcAddress', |
|
args: '', |
|
msg: 'Error isBtcAddress', |
|
}, |
|
{ func: 'isByteLength', args: '', msg: 'Error isByteLength' }, |
|
{ |
|
func: 'isCreditCard', |
|
args: '', |
|
msg: 'Error isCreditCard', |
|
}, |
|
{ |
|
func: 'isCurrency', |
|
args: '', |
|
msg: 'Error isCurrency', |
|
}, |
|
{ func: 'isDataURI', args: '', msg: 'Error isDataURI' }, |
|
{ func: 'isDate', args: '', msg: 'Error isDate' }, |
|
{ |
|
func: 'isDecimal', |
|
args: '', |
|
msg: 'Error isDecimal', |
|
}, |
|
{ func: 'isDivisibleBy', args: '', msg: 'Error isDivisibleBy' }, |
|
{ |
|
func: 'isEAN', |
|
args: '', |
|
msg: 'Error isEAN', |
|
}, |
|
{ |
|
func: 'isEmail', |
|
args: '', |
|
msg: 'Error isEmail', |
|
}, |
|
{ func: 'isEmpty', args: '', msg: 'Error isEmpty' }, |
|
{ |
|
func: 'isEthereumAddress', |
|
args: '', |
|
msg: 'Error isEthereumAddress', |
|
}, |
|
{ |
|
func: 'isFloat', |
|
args: '', |
|
msg: 'Error isFloat', |
|
}, |
|
{ func: 'isFQDN', args: '', msg: 'Error isFQDN' }, |
|
{ func: 'isFullWidth', args: '', msg: 'Error isFullWidth' }, |
|
{ |
|
func: 'isHalfWidth', |
|
args: '', |
|
msg: 'Error isHalfWidth', |
|
}, |
|
{ func: 'isHash', args: '', msg: 'Error isHash' }, |
|
{ |
|
func: 'isHexadecimal', |
|
args: '', |
|
msg: 'Error isHexadecimal', |
|
}, |
|
{ |
|
func: 'isHexColor', |
|
args: '', |
|
msg: 'Error isHexColor', |
|
}, |
|
{ func: 'isHSL', args: '', msg: 'Error isHSL' }, |
|
{ func: 'isIBAN', args: '', msg: 'Error isIBAN' }, |
|
{ |
|
func: 'isIdentityCard', |
|
args: '', |
|
msg: 'Error isIdentityCard', |
|
}, |
|
{ func: 'isIMEI', args: '', msg: 'Error isIMEI' }, |
|
{ |
|
func: 'isIn', |
|
args: '', |
|
msg: 'Error isIn', |
|
}, |
|
{ func: 'isInt', args: '', msg: 'Error isInt' }, |
|
{ |
|
func: 'isIP', |
|
args: '', |
|
msg: 'Error isIP', |
|
}, |
|
{ func: 'isIPRange', args: '', msg: 'Error isIPRange' }, |
|
{ func: 'isISBN', args: '', msg: 'Error isISBN' }, |
|
{ |
|
func: 'isISIN', |
|
args: '', |
|
msg: 'Error isISIN', |
|
}, |
|
{ func: 'isISO8601', args: '', msg: 'Error isISO8601' }, |
|
{ |
|
func: 'isISO31661Alpha2', |
|
args: '', |
|
msg: 'Error isISO31661Alpha2', |
|
}, |
|
{ |
|
func: 'isISO31661Alpha3', |
|
args: '', |
|
msg: 'Error isISO31661Alpha3', |
|
}, |
|
{ func: 'isISRC', args: '', msg: 'Error isISRC' }, |
|
{ |
|
func: 'isISSN', |
|
args: '', |
|
msg: 'Error isISSN', |
|
}, |
|
{ func: 'isJSON', args: '', msg: 'Error isJSON' }, |
|
{ |
|
func: 'isJWT', |
|
args: '', |
|
msg: 'Error isJWT', |
|
}, |
|
{ func: 'isLatLong', args: '', msg: 'Error isLatLong' }, |
|
{ func: 'isLength', args: '', msg: 'Error isLength' }, |
|
{ |
|
func: 'isLocale', |
|
args: '', |
|
msg: 'Error isLocale', |
|
}, |
|
{ func: 'isLowercase', args: '', msg: 'Error isLowercase' }, |
|
{ |
|
func: 'isMACAddress', |
|
args: '', |
|
msg: 'Error isMACAddress', |
|
}, |
|
{ |
|
func: 'isMagnetURI', |
|
args: '', |
|
msg: 'Error isMagnetURI', |
|
}, |
|
{ func: 'isMD5', args: '', msg: 'Error isMD5' }, |
|
{ func: 'isMimeType', args: '', msg: 'Error isMimeType' }, |
|
{ |
|
func: 'isMobilePhone', |
|
args: '', |
|
msg: 'Error isMobilePhone', |
|
}, |
|
{ func: 'isMongoId', args: '', msg: 'Error isMongoId' }, |
|
{ |
|
func: 'isMultibyte', |
|
args: '', |
|
msg: 'Error isMultibyte', |
|
}, |
|
{ |
|
func: 'isNumeric', |
|
args: '', |
|
msg: 'Error isNumeric', |
|
}, |
|
{ func: 'isOctal', args: '', msg: 'Error isOctal' }, |
|
{ |
|
func: 'isPassportNumber', |
|
args: '', |
|
msg: 'Error isPassportNumber', |
|
}, |
|
{ |
|
func: 'isPort', |
|
args: '', |
|
msg: 'Error isPort', |
|
}, |
|
{ func: 'isPostalCode', args: '', msg: 'Error isPostalCode' }, |
|
{ |
|
func: 'isRFC3339', |
|
args: '', |
|
msg: 'Error isRFC3339', |
|
}, |
|
{ |
|
func: 'isRgbColor', |
|
args: '', |
|
msg: 'Error isRgbColor', |
|
}, |
|
{ func: 'isSemVer', args: '', msg: 'Error isSemVer' }, |
|
{ |
|
func: 'isSurrogatePair', |
|
args: '', |
|
msg: 'Error isSurrogatePair', |
|
}, |
|
{ |
|
func: 'isUppercase', |
|
args: '', |
|
msg: 'Error isUppercase', |
|
}, |
|
{ func: 'isSlug', args: '', msg: 'Error isSlug' }, |
|
{ |
|
func: 'isTaxID', |
|
args: '', |
|
msg: 'Error isTaxID', |
|
}, |
|
{ func: 'isURL', args: '', msg: 'Error isURL' }, |
|
{ |
|
func: 'isUUID', |
|
args: '', |
|
msg: 'Error isUUID', |
|
}, |
|
{ func: 'isVariableWidth', args: '', msg: 'Error isVariableWidth' }, |
|
{ |
|
func: 'isWhitelisted', |
|
args: '', |
|
msg: 'Error isWhitelisted', |
|
}, |
|
{ func: 'matches', args: '', msg: 'Error matches' }, |
|
]; |
|
|
|
export default { |
|
name: 'Validation', |
|
props: ['nodes'], |
|
data: () => ({ |
|
fnList: validatorFnList, |
|
columns: null, |
|
validators: [{}, {}], |
|
tableMeta: null, |
|
edited: false, |
|
loading: false, |
|
clickedItem: null, |
|
validatorEditDialog: false, |
|
}), |
|
async created() { |
|
await this.loadTableModelMeta(); |
|
}, |
|
methods: { |
|
editOrAddValidation(item) { |
|
this.clickedItem = JSON.parse(JSON.stringify(item)); |
|
this.validatorEditDialog = true; |
|
}, |
|
|
|
// async loadColumnList() { |
|
// const result = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ |
|
// env: this.nodes.env, |
|
// dbAlias: this.nodes.dbAlias |
|
// }, 'columnList', { |
|
// table_name: this.nodes.table_name |
|
// }]); |
|
// console.log("table ", result.data.list); |
|
// this.columns = result.data.list; |
|
// }, |
|
async loadTableModelMeta() { |
|
this.edited = false; |
|
this.tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [ |
|
{ |
|
env: this.nodes.env, |
|
dbAlias: this.nodes.dbAlias, |
|
}, |
|
'tableXcModelGet', |
|
{ |
|
table_name: this.nodes.table_name, |
|
}, |
|
]); |
|
this.columns = JSON.parse(this.tableMeta.meta); |
|
}, |
|
|
|
scrollAndFocusLastRow() { |
|
this.$nextTick(() => { |
|
const menuActivator = |
|
this.$el && |
|
this.$el.querySelector('.v-expansion-panel--active table tr:last-child .v-small-dialog__activator__content'); |
|
if (menuActivator) { |
|
menuActivator.click(); |
|
this.$nextTick(() => { |
|
const inputField = document.querySelector('.menuable__content__active input'); |
|
inputField && inputField.select(); |
|
}); |
|
} |
|
}); |
|
}, |
|
scrollAndFocusLastRowInModal() { |
|
this.$nextTick(() => { |
|
const modal = document.querySelector('.v-dialog--active'); |
|
modal.scrollTop = 99999; |
|
const menuActivator = modal.querySelector('table tr:last-child .v-small-dialog__activator__content'); |
|
if (menuActivator) { |
|
menuActivator.click(); |
|
this.$nextTick(() => { |
|
const inputField = document.querySelector('.menuable__content__active input'); |
|
inputField && inputField.select(); |
|
}); |
|
} |
|
}); |
|
}, |
|
async saveValidations() { |
|
this.edited = false; |
|
try { |
|
await this.$store.dispatch('sqlMgr/ActSqlOp', [ |
|
{ |
|
env: this.nodes.env, |
|
dbAlias: this.nodes.dbAlias, |
|
}, |
|
'xcModelSet', |
|
{ |
|
table_name: this.nodes.table_name, |
|
meta: this.columns, |
|
}, |
|
]); |
|
this.$toast.success('Successfully updated validations').goAway(3000); |
|
} catch (e) { |
|
this.$toast.error('Failed to update validations').goAway(3000); |
|
} |
|
}, |
|
async saveValidationForColumn(clickedItem) { |
|
if (clickedItem) { |
|
const item = this.columns.columns.find(it => it.column_name === clickedItem.column_name); |
|
if (item) { |
|
Object.assign(item, clickedItem); |
|
await this.saveValidations(); |
|
this.validatorEditDialog = false; |
|
this.clickedItem = null; |
|
} |
|
} |
|
}, |
|
onFunctionChange(i, item) { |
|
this.edited = true; |
|
const fn = validatorFnList.find(({ func }) => func === item.validate.func[i]); |
|
item.validate.msg[ |
|
i |
|
] = `Validation failed : ${item.validate.func[i]}(${this.nodes.table_name}.${item.column_name})`; |
|
item.validate.args[i] = fn.args; |
|
}, |
|
deleteValidation(item, i) { |
|
this.edited = true; |
|
item.validate.func.splice(i, 1); |
|
item.validate.args.splice(i, 1); |
|
item.validate.msg.splice(i, 1); |
|
}, |
|
}, |
|
}; |
|
</script> |
|
|
|
<style scoped></style> |
|
<!-- |
|
/** |
|
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
|
* |
|
* @author Naveen MR <oof1lab@gmail.com> |
|
* @author Pranav C Balan <pranavxc@gmail.com> |
|
* |
|
* @license GNU AGPL version 3 or any later version |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU Affero General Public License as |
|
* published by the Free Software Foundation, either version 3 of the |
|
* License, or (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU Affero General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Affero General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
* |
|
*/ |
|
-->
|
|
|