Merge branch 'master' into Docu-Template-Overview-2021
35
.github/workflows/test.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
||||
##########################################################################
|
||||
- name: Admin | Build `test` image
|
||||
run: |
|
||||
docker build --target test -t "gradido/admin:test" admin/
|
||||
docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
|
||||
docker save "gradido/admin:test" > /tmp/admin.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
@ -294,6 +294,35 @@ jobs:
|
||||
- name: Admin Interface | Lint
|
||||
run: docker run --rm gradido/admin:test yarn run lint
|
||||
|
||||
##############################################################################
|
||||
# JOB: LOCALES ADMIN ######################################################
|
||||
##############################################################################
|
||||
locales_admin:
|
||||
name: Locales - Admin
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_admin]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Admin Interface)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-admin-test
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/admin.tar
|
||||
##########################################################################
|
||||
# LOCALES FRONTEND #######################################################
|
||||
##########################################################################
|
||||
- name: admin | Locales
|
||||
run: docker run --rm gradido/admin:test yarn run locales
|
||||
|
||||
##############################################################################
|
||||
# JOB: LINT BACKEND ##########################################################
|
||||
##############################################################################
|
||||
@ -480,7 +509,7 @@ jobs:
|
||||
- name: backend | docker-compose
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb database
|
||||
- name: backend Unit tests | test
|
||||
run: cd database && yarn && cd ../backend && yarn && yarn test
|
||||
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_worklfow_test
|
||||
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
|
||||
##########################################################################
|
||||
# COVERAGE CHECK BACKEND #################################################
|
||||
@ -491,7 +520,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 37
|
||||
min_coverage: 40
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
|
||||
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
*.log
|
||||
/node_modules/*
|
||||
.vscode
|
||||
messages.pot
|
||||
nbproject
|
||||
.metadata
|
||||
|
||||
3
.vscode/extensions.json
vendored
@ -2,6 +2,7 @@
|
||||
"recommendations": [
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
"esbenp.prettier-vscode",
|
||||
"hediet.vscode-drawio"
|
||||
]
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
@ -13,7 +13,7 @@ ENV BUILD_VERSION="0.0.0.0"
|
||||
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
|
||||
ENV BUILD_COMMIT="0000000"
|
||||
## SET NODE_ENV
|
||||
ENV NODE_ENV="production"
|
||||
ARG NODE_ENV="production"
|
||||
## App relevant Envs
|
||||
ENV PORT="8080"
|
||||
|
||||
|
||||
@ -1,4 +1,15 @@
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-env'],
|
||||
plugins: ['transform-require-context'],
|
||||
module.exports = function (api) {
|
||||
api.cache(true)
|
||||
|
||||
const presets = ['@babel/preset-env']
|
||||
const plugins = []
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
plugins.push('transform-require-context')
|
||||
}
|
||||
|
||||
return {
|
||||
presets,
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
"dev": "yarn run serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "jest --coverage"
|
||||
"test": "jest --coverage",
|
||||
"locales": "scripts/missing-keys.sh && scripts/sort.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
|
||||
17
admin/scripts/missing-keys.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname "$0")/..
|
||||
|
||||
sorting="jq -f $ROOT_DIR/scripts/sort_filter.jq"
|
||||
english="$sorting $ROOT_DIR/src/locales/en.json"
|
||||
german="$sorting $ROOT_DIR/src/locales/de.json"
|
||||
listPaths="jq -c 'path(..)|[.[]|tostring]|join(\".\")'"
|
||||
diffString="<( $english | $listPaths ) <( $german | $listPaths )"
|
||||
if eval "diff -q $diffString";
|
||||
then
|
||||
: # all good
|
||||
else
|
||||
eval "diff -y $diffString | grep '[|<>]'";
|
||||
printf "\nEnglish and German translation keys do not match, see diff above.\n"
|
||||
exit 1
|
||||
fi
|
||||
25
admin/scripts/sort.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
ROOT_DIR=$(dirname "$0")/..
|
||||
|
||||
tmp=$(mktemp)
|
||||
exit_code=0
|
||||
|
||||
for locale_file in $ROOT_DIR/src/locales/*.json
|
||||
do
|
||||
jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp"
|
||||
if [ "$*" == "--fix" ]
|
||||
then
|
||||
mv "$tmp" $locale_file
|
||||
else
|
||||
if diff -q "$tmp" $locale_file > /dev/null ;
|
||||
then
|
||||
: # all good
|
||||
else
|
||||
exit_code=$?
|
||||
echo "$(basename -- $locale_file) is not sorted by keys"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit $exit_code
|
||||
13
admin/scripts/sort_filter.jq
Normal file
@ -0,0 +1,13 @@
|
||||
def walk(f):
|
||||
. as $in
|
||||
| if type == "object" then
|
||||
reduce keys_unsorted[] as $key
|
||||
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
|
||||
elif type == "array" then map( walk(f) ) | f
|
||||
else f
|
||||
end;
|
||||
|
||||
def keys_sort_by(f):
|
||||
to_entries | sort_by(.key|f ) | from_entries;
|
||||
|
||||
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)
|
||||
72
admin/src/components/ConfirmRegisterMailFormular.spec.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue()
|
||||
const toastSuccessMock = jest.fn()
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$toasted: {
|
||||
success: toastSuccessMock,
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
email: 'bob@baumeister.de',
|
||||
dateLastSend: '',
|
||||
}
|
||||
|
||||
describe('ConfirmRegisterMailFormular', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ConfirmRegisterMailFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-confirm-register-mail', () => {
|
||||
expect(wrapper.find('.component-confirm-register-mail').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('send register mail with success', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('button.test-button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API with email', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: { email: 'bob@baumeister.de' },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessMock).toBeCalledWith('unregister_mail.success')
|
||||
})
|
||||
})
|
||||
|
||||
describe('send register mail with error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('button.test-button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('unregister_mail.error')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
56
admin/src/components/ConfirmRegisterMailFormular.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="component-confirm-register-mail">
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<div class="h5">
|
||||
{{ $t('unregister_mail.text', { date: dateLastSend, mail: email }) }}
|
||||
</div>
|
||||
|
||||
<!-- Using components -->
|
||||
<b-input-group :prepend="$t('unregister_mail.info')" class="mt-3">
|
||||
<b-form-input readonly :value="email"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button variant="outline-success" class="test-button" @click="sendRegisterMail">
|
||||
{{ $t('unregister_mail.button') }}
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { sendActivationEmail } from '../graphql/sendActivationEmail'
|
||||
|
||||
export default {
|
||||
name: 'ConfirmRegisterMail',
|
||||
props: {
|
||||
email: {
|
||||
type: String,
|
||||
},
|
||||
dateLastSend: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sendRegisterMail() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: sendActivationEmail,
|
||||
variables: {
|
||||
email: this.email,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email }))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message }))
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.input-group-text {
|
||||
background-color: rgb(255, 252, 205);
|
||||
}
|
||||
</style>
|
||||
@ -3,8 +3,8 @@
|
||||
<hr />
|
||||
<br />
|
||||
<div class="text-center">
|
||||
Gradido Akademie Adminkonsole
|
||||
<div><small>Version: 0.0.1</small></div>
|
||||
{{ $t('gradido_admin_footer') }}
|
||||
<div><small>Version: 1.0.0</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -21,6 +21,7 @@ const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
@ -176,8 +177,8 @@ describe('CreationFormular', () => {
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('sends ... to apollo', () => {
|
||||
expect(toastedErrorMock).toBeCalled()
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="component-creation-formular">
|
||||
{{ $t('creation_form.form') }}
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<b-form ref="creationForm">
|
||||
<b-row class="m-4">
|
||||
<label>Monat Auswählen</label>
|
||||
<label>{{ $t('creation_form.select_month') }}</label>
|
||||
<b-col class="text-left">
|
||||
<b-form-radio
|
||||
id="beforeLastMonth"
|
||||
@ -49,7 +50,7 @@
|
||||
</b-row>
|
||||
|
||||
<b-row class="m-4" v-show="createdIndex != null">
|
||||
<label>Betrag Auswählen</label>
|
||||
<label>{{ $t('creation_form.select_value') }}</label>
|
||||
<div>
|
||||
<b-input-group prepend="GDD" append=".00">
|
||||
<b-form-input
|
||||
@ -72,13 +73,13 @@
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
<label>Text eintragen</label>
|
||||
<label>{{ $t('creation_form.enter_text') }}</label>
|
||||
<div>
|
||||
<b-form-textarea
|
||||
id="textarea-state"
|
||||
v-model="text"
|
||||
:state="text.length >= 10"
|
||||
placeholder="Mindestens 10 Zeichen eingeben"
|
||||
:placeholder="$t('creation_form.min_characters')"
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
</div>
|
||||
@ -86,7 +87,7 @@
|
||||
<b-row class="m-4">
|
||||
<b-col class="text-center">
|
||||
<b-button type="reset" variant="danger" @click="$refs.creationForm.reset()">
|
||||
zurücksetzen
|
||||
{{ $t('creation_form.reset') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-center">
|
||||
@ -99,7 +100,7 @@
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
Update Schöpfung ({{ type }},{{ pagetype }})
|
||||
{{ $t('creation_form.update_creation') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
@ -110,7 +111,7 @@
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
Schöpfung einreichen ({{ type }})
|
||||
{{ $t('creation_form.submit_creation') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-col>
|
||||
@ -170,14 +171,17 @@ export default {
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
long: this.$moment().format('YYYY-MM-DD'),
|
||||
year: this.$moment().format('YYYY'),
|
||||
},
|
||||
lastMonth: {
|
||||
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
|
||||
year: this.$moment().subtract(1, 'month').format('YYYY'),
|
||||
},
|
||||
beforeLastMonth: {
|
||||
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
|
||||
year: this.$moment().subtract(2, 'month').format('YYYY'),
|
||||
},
|
||||
submitObj: null,
|
||||
isdisabled: true,
|
||||
@ -189,6 +193,7 @@ export default {
|
||||
// Auswählen eines Zeitraumes
|
||||
updateRadioSelected(name, index, openCreation) {
|
||||
this.createdIndex = index
|
||||
this.text = this.$t('creation_form.creation_for') + ' ' + name.short + ' ' + name.year
|
||||
// Wenn Mehrfachschöpfung
|
||||
if (this.type === 'massCreation') {
|
||||
// An Creation.vue emitten und radioSelectedMass aktualisieren
|
||||
@ -204,8 +209,6 @@ export default {
|
||||
// Die anzahl der Mitglieder aus der Mehrfachschöpfung
|
||||
const i = Object.keys(this.itemsMassCreation).length
|
||||
// hinweis das eine Mehrfachschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
|
||||
this.submitObj = [
|
||||
{
|
||||
item: this.itemsMassCreation,
|
||||
@ -216,8 +219,6 @@ export default {
|
||||
moderator: this.$store.state.moderator.id,
|
||||
},
|
||||
]
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
|
||||
|
||||
// $store - offene Schöpfungen hochzählen
|
||||
this.$store.commit('openCreationsPlus', i)
|
||||
@ -241,7 +242,10 @@ export default {
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
|
||||
this.$toasted.success(
|
||||
`Offene Schöpfung (${this.value} GDD) für ${this.item.email} wurde gespeichert und liegen zur Bestätigung bereit`,
|
||||
this.$t('creation_form.toasted', {
|
||||
value: this.value,
|
||||
email: this.item.email,
|
||||
}),
|
||||
)
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.submitObj = null
|
||||
|
||||
116
admin/src/components/CreationTransactionListFormular.spec.js
Normal file
@ -0,0 +1,116 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
transactionList: {
|
||||
transactions: [
|
||||
{
|
||||
type: 'created',
|
||||
balance: 100,
|
||||
decayStart: 0,
|
||||
decayEnd: 0,
|
||||
decayDuration: 0,
|
||||
memo: 'Testing',
|
||||
transactionId: 1,
|
||||
name: 'Bibi',
|
||||
email: 'bibi@bloxberg.de',
|
||||
date: new Date(),
|
||||
decay: {
|
||||
balance: 0.01,
|
||||
decayStart: 0,
|
||||
decayEnd: 0,
|
||||
decayDuration: 0,
|
||||
decayStartBlock: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'created',
|
||||
balance: 200,
|
||||
decayStart: 0,
|
||||
decayEnd: 0,
|
||||
decayDuration: 0,
|
||||
memo: 'Testing 2',
|
||||
transactionId: 2,
|
||||
name: 'Bibi',
|
||||
email: 'bibi@bloxberg.de',
|
||||
date: new Date(),
|
||||
decay: {
|
||||
balance: 0.01,
|
||||
decayStart: 0,
|
||||
decayEnd: 0,
|
||||
decayDuration: 0,
|
||||
decayStartBlock: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const toastedErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
global: {
|
||||
error: toastedErrorMock,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
userId: 1,
|
||||
}
|
||||
|
||||
describe('CreationTransactionListFormular', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CreationTransactionListFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('sends query to Apollo when created', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
onlyCreations: true,
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('has two values for the transaction', () => {
|
||||
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
|
||||
})
|
||||
|
||||
describe('query transaction with error', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('toast error', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('OUCH!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
44
admin/src/components/CreationTransactionListFormular.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="component-creation-transaction-list">
|
||||
{{ $t('transactionlist.title') }}
|
||||
<b-table striped hover :items="items"></b-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { transactionList } from '../graphql/transactionList'
|
||||
export default {
|
||||
name: 'CreationTransactionList',
|
||||
props: {
|
||||
userId: { type: Number, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTransactions() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: transactionList,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
onlyCreations: true,
|
||||
userId: parseInt(this.userId),
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.items = result.data.transactionList.transactions
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.global.error(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getTransactions()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -18,6 +18,7 @@ const stateCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
@ -46,9 +47,12 @@ const mocks = {
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
type: '',
|
||||
creation: [],
|
||||
itemsMassCreation: {},
|
||||
creation: [200, 400, 600],
|
||||
creationUserData: {
|
||||
memo: 'Test schöpfung 1',
|
||||
amount: 100,
|
||||
date: '2021-12-01',
|
||||
},
|
||||
}
|
||||
|
||||
describe('EditCreationFormular', () => {
|
||||
@ -67,7 +71,7 @@ describe('EditCreationFormular', () => {
|
||||
expect(wrapper.find('.component-edit-creation-formular').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('radio buttons to selcet month', () => {
|
||||
describe('radio buttons to select month', () => {
|
||||
it('has three radio buttons', () => {
|
||||
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
|
||||
})
|
||||
@ -75,7 +79,7 @@ describe('EditCreationFormular', () => {
|
||||
describe('with single creation', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
|
||||
await wrapper.setProps({ creation: [200, 400, 600] })
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
@ -113,6 +117,26 @@ describe('EditCreationFormular', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('sendForm with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
await wrapper.setData({ rangeMin: 100 })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toast error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -148,23 +172,44 @@ describe('EditCreationFormular', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('sendForm with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
await wrapper.setProps({ creation: [200, 400, 600] })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 100 })
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toast error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('third radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
it('sets rangeMin to 180', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(180)
|
||||
})
|
||||
|
||||
it('sets rangeMax to 400', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(600)
|
||||
it('sets rangeMax to 700', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(700)
|
||||
})
|
||||
|
||||
describe('sendForm', () => {
|
||||
describe('sendForm with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
@ -184,6 +229,26 @@ describe('EditCreationFormular', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendForm with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
await wrapper.setProps({ creation: [200, 400, 600] })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toast error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<b-form ref="updateCreationForm">
|
||||
<b-row class="m-4">
|
||||
<label>Monat Auswählen</label>
|
||||
<label>{{ $t('creation_form.select_month') }}</label>
|
||||
<b-col class="text-left">
|
||||
<b-form-radio
|
||||
id="beforeLastMonth"
|
||||
@ -64,7 +64,7 @@
|
||||
</b-row>
|
||||
|
||||
<b-row class="m-4">
|
||||
<label>Betrag Auswählen</label>
|
||||
<label>{{ $t('creation_form.select_value') }}</label>
|
||||
<div>
|
||||
<b-input-group prepend="GDD" append=".00">
|
||||
<b-form-input
|
||||
@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
<label>Text eintragen</label>
|
||||
<label>{{ $t('creation_form.enter_text') }}</label>
|
||||
<div>
|
||||
<b-form-textarea
|
||||
id="textarea-state"
|
||||
@ -101,7 +101,7 @@
|
||||
<b-row class="m-4">
|
||||
<b-col class="text-center">
|
||||
<b-button type="reset" variant="danger" @click="$refs.updateCreationForm.reset()">
|
||||
zurücksetzen
|
||||
{{ $t('creation_form.reset') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-center">
|
||||
@ -113,7 +113,7 @@
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
Update Schöpfung ({{ type }},{{ pagetype }})
|
||||
{{ $t('creation_form.update_creation') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-col>
|
||||
@ -127,15 +127,6 @@ import { updatePendingCreation } from '../graphql/updatePendingCreation'
|
||||
export default {
|
||||
name: 'EditCreationFormular',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
pagetype: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@ -143,13 +134,6 @@ export default {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
row: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@ -227,7 +211,10 @@ export default {
|
||||
row: this.row,
|
||||
})
|
||||
this.$toasted.success(
|
||||
`Offene schöpfung (${this.value} GDD) für ${this.item.email} wurde geändert, liegt zur Bestätigung bereit`,
|
||||
this.$t('creation_form.toasted_update', {
|
||||
value: this.value,
|
||||
email: this.item.email,
|
||||
}),
|
||||
)
|
||||
this.submitObj = null
|
||||
this.createdIndex = null
|
||||
@ -247,7 +234,7 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.pagetype === 'PageCreationConfirm' && this.creationUserData.date) {
|
||||
if (this.creationUserData.date) {
|
||||
switch (this.$moment(this.creationUserData.date).format('MMMM')) {
|
||||
case this.currentMonth.short:
|
||||
this.createdIndex = 2
|
||||
|
||||
@ -7,6 +7,7 @@ const storeDispatchMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$store: {
|
||||
state: {
|
||||
openCreations: 1,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="component-nabvar">
|
||||
<b-navbar toggleable="sm" type="dark" variant="success">
|
||||
<b-navbar toggleable="md" type="dark" variant="success" class="p-3">
|
||||
<b-navbar-brand to="/">
|
||||
<img src="img/brand/green.png" class="navbar-brand-img" alt="..." />
|
||||
</b-navbar-brand>
|
||||
@ -9,19 +9,18 @@
|
||||
|
||||
<b-collapse id="nav-collapse" is-nav>
|
||||
<b-navbar-nav>
|
||||
<b-nav-item to="/">Übersicht |</b-nav-item>
|
||||
<b-nav-item to="/user">Usersuche |</b-nav-item>
|
||||
<b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item>
|
||||
<b-nav-item to="/">{{ $t('navbar.overview') }}</b-nav-item>
|
||||
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
|
||||
<b-nav-item to="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item>
|
||||
<b-nav-item
|
||||
v-show="$store.state.openCreations > 0"
|
||||
class="h5 bg-danger"
|
||||
class="bg-color-creation p-1"
|
||||
to="/creation-confirm"
|
||||
>
|
||||
| {{ $store.state.openCreations }} offene Schöpfungen
|
||||
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
|
||||
</b-nav-item>
|
||||
<b-nav-item @click="wallet">Wallet</b-nav-item>
|
||||
<b-nav-item @click="logout">Logout</b-nav-item>
|
||||
<!-- <b-nav-item v-show="open < 1" to="/creation-confirm">| keine offene Schöpfungen</b-nav-item> -->
|
||||
<b-nav-item @click="wallet">{{ $t('navbar.wallet') }}</b-nav-item>
|
||||
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
|
||||
</b-navbar-nav>
|
||||
</b-collapse>
|
||||
</b-navbar>
|
||||
@ -49,4 +48,7 @@ export default {
|
||||
height: 2rem;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.bg-color-creation {
|
||||
background-color: #cf1010dc;
|
||||
}
|
||||
</style>
|
||||
|
||||
27
admin/src/components/RowDetails.vue
Normal file
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<b-card class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
|
||||
<b-row class="mb-2">
|
||||
<b-col></b-col>
|
||||
</b-row>
|
||||
<slot :name="slotName" />
|
||||
<b-button size="sm" @click="$emit('row-toogle-details', row, index)">
|
||||
<b-icon
|
||||
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
|
||||
aria-label="Help"
|
||||
></b-icon>
|
||||
{{ $t('hide_details') }} {{ row.item.firstName }} {{ row.item.lastName }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RowDetails',
|
||||
props: {
|
||||
row: { required: true, type: Object },
|
||||
slotName: { requried: true, type: String },
|
||||
type: { requried: true, type: String },
|
||||
index: { requried: true, type: Number },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -13,8 +13,12 @@ describe('UserTable', () => {
|
||||
creation: [],
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(UserTable, { localVue, propsData })
|
||||
return mount(UserTable, { localVue, propsData, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
|
||||
@ -36,63 +36,86 @@
|
||||
hover
|
||||
stacked="md"
|
||||
>
|
||||
<template #cell(creation)="data">
|
||||
<div v-html="data.value"></div>
|
||||
</template>
|
||||
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button
|
||||
variant="info"
|
||||
size="md"
|
||||
@click="editCreationUserTable(row, row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon v-if="row.detailsShowing" icon="x" aria-label="Help"></b-icon>
|
||||
<b-icon v-else icon="pencil-square" aria-label="Help"></b-icon>
|
||||
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
|
||||
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(show_details)="row">
|
||||
<b-button variant="info" size="md" @click="row.toggleDetails" class="mr-2">
|
||||
<b-icon v-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon>
|
||||
<b-icon v-else icon="eye-fill" aria-label="Help"></b-icon>
|
||||
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
|
||||
<b-icon :icon="row.detailsShowing ? 'eye-slash-fill' : 'eye-fill'"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(confirm_mail)="row">
|
||||
<b-button
|
||||
:variant="row.item.emailChecked ? 'success' : 'danger'"
|
||||
size="md"
|
||||
@click="rowToogleDetails(row, 1)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon
|
||||
:icon="row.item.emailChecked ? 'envelope-open' : 'envelope'"
|
||||
aria-label="Help"
|
||||
></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(transactions_list)="row">
|
||||
<b-button variant="warning" size="md" @click="rowToogleDetails(row, 2)" class="mr-2">
|
||||
<b-icon icon="list"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #row-details="row">
|
||||
<b-card class="shadow-lg p-3 mb-5 bg-white rounded">
|
||||
<b-row class="mb-2">
|
||||
<b-col></b-col>
|
||||
</b-row>
|
||||
{{ type }}
|
||||
<creation-formular
|
||||
v-if="type === 'PageUserSearch'"
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
<edit-creation-formular
|
||||
v-else
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:row="row"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
|
||||
<b-button size="sm" @click="row.toggleDetails">
|
||||
<b-icon
|
||||
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
|
||||
aria-label="Help"
|
||||
></b-icon>
|
||||
Details verbergen von {{ row.item.firstName }} {{ row.item.lastName }}
|
||||
</b-button>
|
||||
</b-card>
|
||||
<row-details
|
||||
:row="row"
|
||||
:type="type"
|
||||
:slotName="slotName"
|
||||
:index="slotIndex"
|
||||
@row-toogle-details="rowToogleDetails"
|
||||
>
|
||||
<template #show-creation>
|
||||
<div>
|
||||
<creation-formular
|
||||
v-if="type === 'PageUserSearch'"
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
<edit-creation-formular
|
||||
v-else
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:row="row"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #show-register-mail>
|
||||
<confirm-register-mail-formular
|
||||
:email="row.item.email"
|
||||
:dateLastSend="$moment().subtract(1, 'month').format('dddd, DD.MMMM.YYYY HH:mm'),"
|
||||
/>
|
||||
</template>
|
||||
<template #show-transaction-list>
|
||||
<creation-transaction-list-formular :userId="row.item.userId" />
|
||||
</template>
|
||||
</row-details>
|
||||
</template>
|
||||
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button
|
||||
variant="warning"
|
||||
@ -132,8 +155,14 @@
|
||||
<script>
|
||||
import CreationFormular from '../components/CreationFormular.vue'
|
||||
import EditCreationFormular from '../components/EditCreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../components/ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionListFormular from '../components/CreationTransactionListFormular.vue'
|
||||
import RowDetails from '../components/RowDetails.vue'
|
||||
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
|
||||
const slotNames = ['show-creation', 'show-register-mail', 'show-transaction-list']
|
||||
|
||||
export default {
|
||||
name: 'UserTable',
|
||||
props: {
|
||||
@ -162,9 +191,15 @@ export default {
|
||||
components: {
|
||||
CreationFormular,
|
||||
EditCreationFormular,
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionListFormular,
|
||||
RowDetails,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreationFormular: null,
|
||||
showConfirmRegisterMailFormular: null,
|
||||
showCreationTransactionListFormular: null,
|
||||
creationUserData: {},
|
||||
overlay: false,
|
||||
overlayBookmarkType: '',
|
||||
@ -178,30 +213,53 @@ export default {
|
||||
button_cancel: 'Cancel',
|
||||
},
|
||||
],
|
||||
slotIndex: 0,
|
||||
openRow: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowToogleDetails(row, index) {
|
||||
if (this.openRow) {
|
||||
if (this.openRow.index === row.index) {
|
||||
if (index === this.slotIndex) {
|
||||
row.toggleDetails()
|
||||
this.openRow = null
|
||||
} else {
|
||||
this.slotIndex = index
|
||||
}
|
||||
} else {
|
||||
this.openRow.toggleDetails()
|
||||
row.toggleDetails()
|
||||
this.slotIndex = index
|
||||
this.openRow = row
|
||||
}
|
||||
} else {
|
||||
row.toggleDetails()
|
||||
this.slotIndex = index
|
||||
this.openRow = row
|
||||
if (this.type === 'PageCreationConfirm') {
|
||||
this.creationUserData = row.item
|
||||
}
|
||||
}
|
||||
},
|
||||
overlayShow(bookmarkType, item) {
|
||||
this.overlay = true
|
||||
this.overlayBookmarkType = bookmarkType
|
||||
this.overlayItem = item
|
||||
|
||||
if (bookmarkType === 'remove') {
|
||||
this.overlayText.header = 'Achtung! Schöpfung löschen!'
|
||||
this.overlayText.text1 =
|
||||
'Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.'
|
||||
this.overlayText.text2 = 'Willst du die vorgespeicherte Schöpfung wirklich löschen? '
|
||||
this.overlayText.button_ok = 'Ja, Schöpfung löschen!'
|
||||
this.overlayText.button_cancel = 'Nein, nicht löschen.'
|
||||
this.overlayText.header = this.$t('overlay.remove.title')
|
||||
this.overlayText.text1 = this.$t('overlay.remove.text')
|
||||
this.overlayText.text2 = this.$t('overlay.remove.question')
|
||||
this.overlayText.button_ok = this.$t('overlay.remove.yes')
|
||||
this.overlayText.button_cancel = this.$t('overlay.remove.no')
|
||||
}
|
||||
if (bookmarkType === 'confirm') {
|
||||
this.overlayText.header = 'Schöpfung bestätigen!'
|
||||
this.overlayText.text1 =
|
||||
'Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.'
|
||||
this.overlayText.text2 =
|
||||
'Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?'
|
||||
this.overlayText.button_ok = 'Ja, Schöpfung speichern und bestätigen!'
|
||||
this.overlayText.button_cancel = 'Nein, nicht speichern.'
|
||||
this.overlayText.header = this.$t('overlay.confirm.title')
|
||||
this.overlayText.text1 = this.$t('overlay.confirm.text')
|
||||
this.overlayText.text2 = this.$t('overlay.confirm.question')
|
||||
this.overlayText.button_ok = this.$t('overlay.confirm.yes')
|
||||
this.overlayText.button_cancel = this.$t('overlay.confirm.no')
|
||||
}
|
||||
},
|
||||
overlayOK(bookmarkType, item) {
|
||||
@ -237,20 +295,12 @@ export default {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$emit('remove-confirm-result', item, 'remove')
|
||||
this.$emit('remove-confirm-result', item, 'confirmed')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
},
|
||||
editCreationUserTable(row, rowItem) {
|
||||
if (!row.detailsShowing) {
|
||||
this.creationUserData = rowItem
|
||||
} else {
|
||||
this.creationUserData = {}
|
||||
}
|
||||
row.toggleDetails()
|
||||
},
|
||||
updateCreationData(data) {
|
||||
this.creationUserData.amount = data.amount
|
||||
this.creationUserData.date = data.date
|
||||
@ -263,6 +313,11 @@ export default {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
slotName() {
|
||||
return slotNames[this.slotIndex]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@ -3,7 +3,7 @@ import gql from 'graphql-tag'
|
||||
export const createPendingCreation = gql`
|
||||
mutation (
|
||||
$email: String!
|
||||
$amount: Int!
|
||||
$amount: Float!
|
||||
$memo: String!
|
||||
$creationDate: String!
|
||||
$moderator: Int!
|
||||
|
||||
@ -3,10 +3,12 @@ import gql from 'graphql-tag'
|
||||
export const searchUsers = gql`
|
||||
query ($searchText: String!) {
|
||||
searchUsers(searchText: $searchText) {
|
||||
userId
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
creation
|
||||
emailChecked
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
7
admin/src/graphql/sendActivationEmail.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const sendActivationEmail = gql`
|
||||
mutation ($email: String!) {
|
||||
sendActivationEmail(email: $email)
|
||||
}
|
||||
`
|
||||
44
admin/src/graphql/transactionList.js
Normal file
@ -0,0 +1,44 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const transactionList = gql`
|
||||
query (
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$onlyCreations: Boolean = false
|
||||
$userId: Int = null
|
||||
) {
|
||||
transactionList(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
onlyCreations: $onlyCreations
|
||||
userId: $userId
|
||||
) {
|
||||
gdtSum
|
||||
count
|
||||
balance
|
||||
decay
|
||||
decayDate
|
||||
transactions {
|
||||
type
|
||||
balance
|
||||
decayStart
|
||||
decayEnd
|
||||
decayDuration
|
||||
memo
|
||||
transactionId
|
||||
name
|
||||
email
|
||||
date
|
||||
decay {
|
||||
balance
|
||||
decayStart
|
||||
decayEnd
|
||||
decayDuration
|
||||
decayStartBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -4,7 +4,7 @@ export const updatePendingCreation = gql`
|
||||
mutation (
|
||||
$id: Int!
|
||||
$email: String!
|
||||
$amount: Int!
|
||||
$amount: Float!
|
||||
$memo: String!
|
||||
$creationDate: String!
|
||||
$moderator: Int!
|
||||
|
||||
@ -4,7 +4,7 @@ import VueI18n from 'vue-i18n'
|
||||
Vue.use(VueI18n)
|
||||
|
||||
const loadLocaleMessages = () => {
|
||||
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const locales = require.context('./locales/', true, /[A-Za-z0-9-_,\s]+\.json$/i)
|
||||
const messages = {}
|
||||
locales.keys().forEach((key) => {
|
||||
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
|
||||
|
||||
@ -1 +1,65 @@
|
||||
{}
|
||||
{
|
||||
"bookmark": "bookmark",
|
||||
"confirmed": "bestätigt",
|
||||
"creation_form": {
|
||||
"creation_for": "Schöpfung für ",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
"min_characters": "Mindestens 10 Zeichen eingeben",
|
||||
"reset": "Zurücksetzen",
|
||||
"select_month": "Monat auswählen",
|
||||
"select_value": "Betrag auswählen",
|
||||
"submit_creation": "Schöpfung einreichen",
|
||||
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
|
||||
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
|
||||
"update_creation": "Schöpfung aktualisieren"
|
||||
},
|
||||
"details": "Details",
|
||||
"e_mail": "E-Mail",
|
||||
"firstname": "Vorname",
|
||||
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
|
||||
"hide_details": "Details verbergen von",
|
||||
"lastname": "Nachname",
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
"navbar": {
|
||||
"logout": "Abmelden",
|
||||
"multi_creation": "Mehrfachschöpfung",
|
||||
"open_creation": "Offene Schöpfungen",
|
||||
"overview": "Übersicht",
|
||||
"user_search": "Nutzersuche",
|
||||
"wallet": "Wallet"
|
||||
},
|
||||
"not_open_creations": "Keine offenen Schöpfungen",
|
||||
"open_creation": "Offene Schöpfung",
|
||||
"open_creations": "Offene Schöpfungen",
|
||||
"overlay": {
|
||||
"confirm": {
|
||||
"no": "Nein, nicht speichern.",
|
||||
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?",
|
||||
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
|
||||
"title": "Schöpfung bestätigen!",
|
||||
"yes": "Ja, Schöpfung bestätigen und speichern!"
|
||||
},
|
||||
"remove": {
|
||||
"no": "Nein, nicht löschen.",
|
||||
"question": "Willst du die vorgespeicherte Schöpfung wirklich löschen?",
|
||||
"text": "Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.",
|
||||
"title": "Achtung! Schöpfung löschen!",
|
||||
"yes": "Ja, Schöpfung löschen!"
|
||||
}
|
||||
},
|
||||
"remove": "Entfernen",
|
||||
"transaction": "Transaktion",
|
||||
"transactionlist": {
|
||||
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
||||
},
|
||||
"unregistered_emails": "Unregistrierte E-Mails",
|
||||
"unregister_mail": {
|
||||
"button": "Registrierungs-Email bestätigen, jetzt senden",
|
||||
"error": "Fehler beim Senden des Bestätigungs-Links an den Benutzer: {message}",
|
||||
"info": "Email bestätigen, wiederholt senden an:",
|
||||
"success": "Erfolgreiches Senden des Bestätigungs-Links an die E-Mail des Nutzers! ({email})",
|
||||
"text": " Die letzte Email wurde am <b>{date} Uhr</b> an das Mitglied ({mail}) gesendet."
|
||||
},
|
||||
"user_search": "Nutzer-Suche"
|
||||
}
|
||||
|
||||
@ -1 +1,65 @@
|
||||
{}
|
||||
{
|
||||
"bookmark": "Remember",
|
||||
"confirmed": "confirmed",
|
||||
"creation_form": {
|
||||
"creation_for": "Creation for ",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
"min_characters": "Enter at least 10 characters",
|
||||
"reset": "Reset",
|
||||
"select_month": "Select month",
|
||||
"select_value": "Select amount",
|
||||
"submit_creation": "Submit creation",
|
||||
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
|
||||
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
|
||||
"update_creation": "Creation update"
|
||||
},
|
||||
"details": "Details",
|
||||
"e_mail": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"gradido_admin_footer": "Gradido Academy Admin Console",
|
||||
"hide_details": "Hide details from",
|
||||
"lastname": "Lastname",
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
"navbar": {
|
||||
"logout": "Logout",
|
||||
"multi_creation": "Multiple creation",
|
||||
"open_creation": "Open creations",
|
||||
"overview": "Overview",
|
||||
"user_search": "User search",
|
||||
"wallet": "Wallet"
|
||||
},
|
||||
"not_open_creations": "No open creations",
|
||||
"open_creation": "Open creation",
|
||||
"open_creations": "Open creations",
|
||||
"overlay": {
|
||||
"confirm": {
|
||||
"no": "No, do not save.",
|
||||
"question": "Do you really want to carry out and finally save this pre-stored creation?",
|
||||
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
|
||||
"title": "Confirm creation!",
|
||||
"yes": "Yes, confirm and save creation!"
|
||||
},
|
||||
"remove": {
|
||||
"no": "No, do not delete.",
|
||||
"question": "Do you really want to delete the pre-stored creation?",
|
||||
"text": "After deletion, there is no possibility to restore this data record. However, the entire process is saved in the log file as an overview.",
|
||||
"title": "Attention! Delete creation!",
|
||||
"yes": "Yes, delete creation!"
|
||||
}
|
||||
},
|
||||
"remove": "Remove",
|
||||
"transaction": "Transaction",
|
||||
"transactionlist": {
|
||||
"title": "All creation-transactions for the user"
|
||||
},
|
||||
"unregistered_emails": "Unregistered e-mails",
|
||||
"unregister_mail": {
|
||||
"button": "Confirm registration email, send now",
|
||||
"error": "Error sending the confirmation link to the user: {message}",
|
||||
"info": "Confirm email, send repeatedly to:",
|
||||
"success": "Successfully send the confirmation link to the user's email! ({email})",
|
||||
"text": " The last email was sent to the member ({mail}) on <b>{date} clock</b>."
|
||||
},
|
||||
"user_search": "User search"
|
||||
}
|
||||
|
||||
16
admin/src/locales/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
const locales = [
|
||||
{
|
||||
name: 'English',
|
||||
code: 'en',
|
||||
iso: 'en-US',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Deutsch',
|
||||
code: 'de',
|
||||
iso: 'de-DE',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
export default locales
|
||||
@ -28,6 +28,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
@update-item="updateItem"
|
||||
/>
|
||||
<div v-if="itemsMassCreation.length === 0">
|
||||
Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest
|
||||
{{ $t('multiple_creation_text') }}
|
||||
</div>
|
||||
<creation-formular
|
||||
v-else
|
||||
@ -40,7 +40,6 @@
|
||||
:items="itemsMassCreation"
|
||||
@remove-all-bookmark="removeAllBookmark"
|
||||
/>
|
||||
{{ itemsMassCreation }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
@ -60,18 +59,18 @@ export default {
|
||||
return {
|
||||
showArrays: false,
|
||||
Searchfields: [
|
||||
{ key: 'bookmark', label: 'merken' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{ key: 'creation', label: 'Creation' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'bookmark', label: 'bookmark' },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{ key: 'creation', label: this.$t('open_creations') },
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
],
|
||||
fields: [
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{ key: 'creation', label: 'Creation' },
|
||||
{ key: 'bookmark', label: 'löschen' },
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{ key: 'creation', label: this.$t('open_creations') },
|
||||
{ key: 'bookmark', label: this.$t('remove') },
|
||||
],
|
||||
itemsList: [],
|
||||
itemsMassCreation: [],
|
||||
|
||||
@ -37,6 +37,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
@ -81,7 +82,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation delete with success', () => {
|
||||
describe('delete creation delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
@ -130,7 +131,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation delete with error', () => {
|
||||
describe('delete creation delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper
|
||||
@ -143,6 +144,67 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
{
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergatert',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirmed')
|
||||
})
|
||||
|
||||
it('calls the deletePendingCreation mutation', () => {
|
||||
expect(apolloMutateMock).not.toBeCalledWith({
|
||||
mutation: deletePendingCreation,
|
||||
variables: { id: 1 },
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete creation delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirm')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Case confirm is not supported')
|
||||
})
|
||||
})
|
||||
|
||||
describe('server response is error', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
@ -51,25 +51,34 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
removeConfirmResult(e, event) {
|
||||
if (event === 'remove') {
|
||||
let index = 0
|
||||
const findArr = this.confirmResult.find((arr) => arr.id === e.id)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deletePendingCreation,
|
||||
variables: {
|
||||
id: findArr.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
index = this.confirmResult.indexOf(findArr)
|
||||
this.confirmResult.splice(index, 1)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
this.$toasted.success('Pending Creation has been deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
let index = 0
|
||||
const findArr = this.confirmResult.find((arr) => arr.id === e.id)
|
||||
switch (event) {
|
||||
case 'remove':
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deletePendingCreation,
|
||||
variables: {
|
||||
id: findArr.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
index = this.confirmResult.indexOf(findArr)
|
||||
this.confirmResult.splice(index, 1)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
this.$toasted.success('Pending Creation has been deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
break
|
||||
case 'confirmed':
|
||||
this.confirmResult.splice(index, 1)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
this.$toasted.success('Pending Creation has been deleted')
|
||||
break
|
||||
default:
|
||||
this.$toasted.error('Case ' + event + ' is not supported')
|
||||
}
|
||||
},
|
||||
getPendingCreations() {
|
||||
@ -80,7 +89,7 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('resetOpenCreations')
|
||||
this.confirmResult = result.data.getPendingCreations.reverse()
|
||||
this.confirmResult = result.data.getPendingCreations
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@ -22,6 +22,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<b-card
|
||||
v-show="$store.state.openCreations > 0"
|
||||
border-variant="primary"
|
||||
header="offene Schöpfungen"
|
||||
:header="$t('open_creations')"
|
||||
header-bg-variant="danger"
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
@ -17,7 +17,7 @@
|
||||
<b-card
|
||||
v-show="$store.state.openCreations < 1"
|
||||
border-variant="success"
|
||||
header="keine offene Schöpfungen"
|
||||
:header="$t('not_open_creations')"
|
||||
header-bg-variant="success"
|
||||
header-text-variant="white"
|
||||
align="center"
|
||||
@ -28,28 +28,6 @@
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
<b-list-group>
|
||||
<b-list-group-item class="bg-secondary text-light" href="user">
|
||||
zur Usersuche
|
||||
</b-list-group-item>
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
Mitglieder
|
||||
<b-badge class="bg-success" pill>2400</b-badge>
|
||||
</b-list-group-item>
|
||||
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
aktive Mitglieder
|
||||
<b-badge class="bg-primary" pill>2201</b-badge>
|
||||
</b-list-group-item>
|
||||
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
nicht bestätigte Mitglieder
|
||||
<b-badge class="bg-warning text-dark" pill>120</b-badge>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -11,6 +11,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
emailChecked: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -19,12 +20,23 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
describe('UserSearch', () => {
|
||||
@ -43,6 +55,16 @@ describe('UserSearch', () => {
|
||||
expect(wrapper.find('div.user-search').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('unconfirmed emails', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.btn-block').trigger('click')
|
||||
})
|
||||
|
||||
it('filters the users by unconfirmed emails', () => {
|
||||
expect(wrapper.vm.searchResult).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('apollo returns error', () => {
|
||||
beforeEach(() => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
<template>
|
||||
<div class="user-search">
|
||||
<label>Usersuche</label>
|
||||
<div style="text-align: right">
|
||||
<b-button block variant="danger" @click="unconfirmedRegisterMails">
|
||||
<b-icon icon="envelope" variant="light"></b-icon>
|
||||
{{ $t('unregistered_emails') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<label>{{ $t('user_search') }}</label>
|
||||
<b-input
|
||||
type="text"
|
||||
v-model="criteria"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
placeholder="User suche"
|
||||
class="shadow p-3 mb-3 bg-white rounded"
|
||||
:placeholder="$t('user_search')"
|
||||
@input="getUsers"
|
||||
></b-input>
|
||||
|
||||
<user-table
|
||||
type="PageUserSearch"
|
||||
:itemsUser="searchResult"
|
||||
:fieldsTable="fields"
|
||||
:criteria="criteria"
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -29,25 +37,59 @@ export default {
|
||||
return {
|
||||
showArrays: false,
|
||||
fields: [
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Firstname' },
|
||||
{ key: 'lastName', label: 'Lastname' },
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
label: 'Creation',
|
||||
label: this.$t('open_creation'),
|
||||
formatter: (value, key, item) => {
|
||||
return String(value)
|
||||
return (
|
||||
`
|
||||
<div>` +
|
||||
this.$moment().subtract(2, 'month').format('MMMM') +
|
||||
` - ` +
|
||||
String(value[0]) +
|
||||
` GDD</div>
|
||||
<div>` +
|
||||
this.$moment().subtract(1, 'month').format('MMMM') +
|
||||
` - ` +
|
||||
String(value[1]) +
|
||||
` GDD</div>
|
||||
<div>` +
|
||||
this.$moment().format('MMMM') +
|
||||
` - ` +
|
||||
String(value[2]) +
|
||||
` GDD</div>
|
||||
`
|
||||
)
|
||||
},
|
||||
},
|
||||
{ key: 'show_details', label: 'Details' },
|
||||
{ key: 'show_details', label: this.$t('details') },
|
||||
{ key: 'confirm_mail', label: this.$t('confirmed') },
|
||||
{ key: 'transactions_list', label: this.$t('transaction') },
|
||||
],
|
||||
searchResult: [],
|
||||
massCreation: [],
|
||||
criteria: '',
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
},
|
||||
lastMonth: {
|
||||
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||
},
|
||||
beforeLastMonth: {
|
||||
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
unconfirmedRegisterMails() {
|
||||
this.searchResult = this.searchResult.filter((user) => {
|
||||
return user.emailChecked
|
||||
})
|
||||
},
|
||||
getUsers() {
|
||||
this.$apollo
|
||||
.query({
|
||||
@ -57,12 +99,7 @@ export default {
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.searchResult = result.data.searchUsers.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
// showDetails: true,
|
||||
}
|
||||
})
|
||||
this.searchResult = result.data.searchUsers
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
|
||||
@ -15,6 +15,9 @@ DB_DATABASE=gradido_community
|
||||
#EMAIL_PASSWORD=
|
||||
#EMAIL_SMTP_URL=
|
||||
#EMAIL_SMTP_PORT=587
|
||||
#RESEND_TIME=1 minute, 60 => 1hour, 1440 (60 minutes * 24 hours) => 24 hours
|
||||
#RESEND_TIME=
|
||||
RESEND_TIME=10
|
||||
|
||||
#EMAIL_LINK_VERIFICATION=http://localhost/vue/checkEmail/$1
|
||||
|
||||
|
||||
@ -5,6 +5,11 @@ module.exports = {
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
|
||||
moduleNameMapper: {
|
||||
'@entity/(.*)': '<rootDir>/../database/entity/$1',
|
||||
'@entity/(.*)': '<rootDir>/../database/build/entity/$1',
|
||||
// This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
|
||||
'@dbTools/(.*)':
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
},
|
||||
}
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
"start": "node build/index.js",
|
||||
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
|
||||
"lint": "eslint . --ext .js,.ts",
|
||||
"test": "jest --runInBand --coverage "
|
||||
"CI_worklfow_test": "jest --runInBand --coverage ",
|
||||
"test": "NODE_ENV=development jest --runInBand --coverage "
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
@ -59,6 +60,7 @@
|
||||
"typescript": "^4.3.4"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"@entity": "../database/build/entity"
|
||||
"@entity": "../database/build/entity",
|
||||
"@dbTools": "../database/build/src"
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ const loginServer = {
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
}
|
||||
|
||||
const resendTime = parseInt(process.env.RESEND_TIME ? process.env.RESEND_TIME : 'null')
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||
@ -52,6 +53,7 @@ const email = {
|
||||
EMAIL_LINK_VERIFICATION:
|
||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
|
||||
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1',
|
||||
RESEND_TIME: isNaN(resendTime) ? 10 : resendTime,
|
||||
}
|
||||
|
||||
const webhook = {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import { ArgsType, Field, Float, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class CreatePendingCreationArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Int)
|
||||
@Field(() => Float)
|
||||
amount: number
|
||||
|
||||
@Field(() => String)
|
||||
|
||||
@ -11,9 +11,6 @@ export default class CreateUserArgs {
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
|
||||
@Field(() => String)
|
||||
language?: string // Will default to DEFAULT_LANGUAGE
|
||||
|
||||
|
||||
@ -11,4 +11,10 @@ export default class Paginated {
|
||||
|
||||
@Field(() => Order, { nullable: true })
|
||||
order?: Order
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
onlyCreations?: boolean
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
userId?: number
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import { ArgsType, Field, Float, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class CreatePendingCreationArgs {
|
||||
@ -8,7 +8,7 @@ export default class CreatePendingCreationArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Int)
|
||||
@Field(() => Float)
|
||||
amount: number
|
||||
|
||||
@Field(() => String)
|
||||
|
||||
@ -2,6 +2,9 @@ import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class UserAdmin {
|
||||
@Field(() => Number)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@ -13,4 +16,7 @@ export class UserAdmin {
|
||||
|
||||
@Field(() => [Number])
|
||||
creation: number[]
|
||||
|
||||
@Field(() => Boolean)
|
||||
emailChecked: boolean
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import { UserTransaction } from '@entity/UserTransaction'
|
||||
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
|
||||
import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@ -28,10 +29,12 @@ export class AdminResolver {
|
||||
const adminUsers = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const adminUser = new UserAdmin()
|
||||
adminUser.userId = user.id
|
||||
adminUser.firstName = user.firstName
|
||||
adminUser.lastName = user.lastName
|
||||
adminUser.email = user.email
|
||||
adminUser.creation = await getUserCreations(user.id)
|
||||
adminUser.emailChecked = await hasActivatedEmail(user.email)
|
||||
return adminUser
|
||||
}),
|
||||
)
|
||||
@ -133,7 +136,7 @@ export class AdminResolver {
|
||||
return newPendingCreation
|
||||
}),
|
||||
)
|
||||
return pendingCreationsPromise
|
||||
return pendingCreationsPromise.reverse()
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@ -212,95 +215,80 @@ export class AdminResolver {
|
||||
|
||||
async function getUserCreations(id: number): Promise<number[]> {
|
||||
const dateNextMonth = moment().add(1, 'month').format('YYYY-MM') + '-01'
|
||||
const dateMonth = moment().format('YYYY-MM') + '-01'
|
||||
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM') + '-01'
|
||||
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM') + '-01'
|
||||
const beforeLastMonthNumber = moment().subtract(2, 'month').format('M')
|
||||
const lastMonthNumber = moment().subtract(1, 'month').format('M')
|
||||
const currentMonthNumber = moment().format('M')
|
||||
|
||||
const transactionCreationRepository = getCustomRepository(TransactionCreationRepository)
|
||||
const createdAmountBeforeLastMonth = await transactionCreationRepository
|
||||
const createdAmountsQuery = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('SUM(transaction_creations.amount)', 'sum')
|
||||
.select('MONTH(transaction_creations.target_date)', 'target_month')
|
||||
.addSelect('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
|
||||
date: dateBeforeLastMonth,
|
||||
enddate: dateLastMonth,
|
||||
endDate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const createdAmountLastMonth = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateLastMonth,
|
||||
enddate: dateMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const createdAmountMonth = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateMonth,
|
||||
enddate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
.groupBy('target_month')
|
||||
.orderBy('target_month', 'ASC')
|
||||
.getRawMany()
|
||||
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const pendingAmountMounth = await loginPendingTasksAdminRepository
|
||||
const pendingAmountsQuery = await loginPendingTasksAdminRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.select('MONTH(login_pending_tasks_admin.date)', 'target_month')
|
||||
.addSelect('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateMonth,
|
||||
enddate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const pendingAmountLastMounth = await loginPendingTasksAdminRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: dateLastMonth,
|
||||
enddate: dateMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
|
||||
const pendingAmountBeforeLastMounth = await loginPendingTasksAdminRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :enddate`, {
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
|
||||
date: dateBeforeLastMonth,
|
||||
enddate: dateLastMonth,
|
||||
endDate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.getRawOne()
|
||||
.groupBy('target_month')
|
||||
.orderBy('target_month', 'ASC')
|
||||
.getRawMany()
|
||||
|
||||
const map = new Map()
|
||||
if (Array.isArray(createdAmountsQuery) && createdAmountsQuery.length > 0) {
|
||||
createdAmountsQuery.forEach((createdAmount) => {
|
||||
if (!map.has(createdAmount.target_month)) {
|
||||
map.set(createdAmount.target_month, createdAmount.sum)
|
||||
} else {
|
||||
const store = map.get(createdAmount.target_month)
|
||||
map.set(createdAmount.target_month, Number(store) + Number(createdAmount.sum))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(pendingAmountsQuery) && pendingAmountsQuery.length > 0) {
|
||||
pendingAmountsQuery.forEach((pendingAmount) => {
|
||||
if (!map.has(pendingAmount.target_month)) {
|
||||
map.set(pendingAmount.target_month, pendingAmount.sum)
|
||||
} else {
|
||||
const store = map.get(pendingAmount.target_month)
|
||||
map.set(pendingAmount.target_month, Number(store) + Number(pendingAmount.sum))
|
||||
}
|
||||
})
|
||||
}
|
||||
const usedCreationBeforeLastMonth = map.get(Number(beforeLastMonthNumber))
|
||||
? Number(map.get(Number(beforeLastMonthNumber))) / 10000
|
||||
: 0
|
||||
const usedCreationLastMonth = map.get(Number(lastMonthNumber))
|
||||
? Number(map.get(Number(lastMonthNumber))) / 10000
|
||||
: 0
|
||||
|
||||
const usedCreationCurrentMonth = map.get(Number(currentMonthNumber))
|
||||
? Number(map.get(Number(currentMonthNumber))) / 10000
|
||||
: 0
|
||||
|
||||
// COUNT amount from 2 tables
|
||||
const usedCreationBeforeLastMonth =
|
||||
(Number(createdAmountBeforeLastMonth.sum) + Number(pendingAmountBeforeLastMounth.sum)) / 10000
|
||||
const usedCreationLastMonth =
|
||||
(Number(createdAmountLastMonth.sum) + Number(pendingAmountLastMounth.sum)) / 10000
|
||||
const usedCreationMonth =
|
||||
(Number(createdAmountMonth.sum) + Number(pendingAmountMounth.sum)) / 10000
|
||||
return [
|
||||
1000 - usedCreationBeforeLastMonth,
|
||||
1000 - usedCreationLastMonth,
|
||||
1000 - usedCreationMonth,
|
||||
1000 - usedCreationCurrentMonth,
|
||||
]
|
||||
}
|
||||
|
||||
@ -330,3 +318,8 @@ function isCreationValid(creations: number[], amount: number, creationDate: Date
|
||||
}
|
||||
return true
|
||||
}
|
||||
async function hasActivatedEmail(email: string): Promise<boolean> {
|
||||
const repository = getCustomRepository(LoginUserRepository)
|
||||
const user = await repository.findByEmail(email)
|
||||
return user ? user.emailChecked : false
|
||||
}
|
||||
|
||||
@ -340,6 +340,7 @@ async function listTransactions(
|
||||
pageSize: number,
|
||||
order: Order,
|
||||
user: dbUser,
|
||||
onlyCreations: boolean,
|
||||
): Promise<TransactionList> {
|
||||
let limit = pageSize
|
||||
let offset = 0
|
||||
@ -358,6 +359,7 @@ async function listTransactions(
|
||||
limit,
|
||||
offset,
|
||||
order,
|
||||
onlyCreations,
|
||||
)
|
||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
||||
const decay = !(currentPage > 1)
|
||||
@ -469,14 +471,32 @@ export class TransactionResolver {
|
||||
@Authorized([RIGHTS.TRANSACTION_LIST])
|
||||
@Query(() => TransactionList)
|
||||
async transactionList(
|
||||
@Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||
@Args()
|
||||
{
|
||||
currentPage = 1,
|
||||
pageSize = 25,
|
||||
order = Order.DESC,
|
||||
onlyCreations = false,
|
||||
userId,
|
||||
}: Paginated,
|
||||
@Ctx() context: any,
|
||||
): Promise<TransactionList> {
|
||||
// load user
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
let userEntity: dbUser | undefined
|
||||
if (userId) {
|
||||
userEntity = await userRepository.findOneOrFail({ id: userId })
|
||||
} else {
|
||||
userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
}
|
||||
|
||||
const transactions = await listTransactions(currentPage, pageSize, order, userEntity)
|
||||
const transactions = await listTransactions(
|
||||
currentPage,
|
||||
pageSize,
|
||||
order,
|
||||
userEntity,
|
||||
onlyCreations,
|
||||
)
|
||||
|
||||
// get gdt sum
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
|
||||
232
backend/src/graphql/resolver/UserResolver.test.ts
Normal file
@ -0,0 +1,232 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import gql from 'graphql-tag'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import createServer from '../../server/createServer'
|
||||
import { resetDB, initialize } from '@dbTools/helpers'
|
||||
import { getRepository } from 'typeorm'
|
||||
import { LoginUser } from '@entity/LoginUser'
|
||||
import { LoginUserBackup } from '@entity/LoginUserBackup'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '../../config'
|
||||
import { sendEMail } from '../../util/sendEMail'
|
||||
|
||||
jest.mock('../../util/sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
let mutate: any
|
||||
let con: any
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = await createServer({})
|
||||
con = server.con
|
||||
mutate = createTestClient(server.apollo).mutate
|
||||
await initialize()
|
||||
await resetDB()
|
||||
})
|
||||
|
||||
describe('UserResolver', () => {
|
||||
describe('createUser', () => {
|
||||
const variables = {
|
||||
email: 'peter@lustig.de',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
language: 'de',
|
||||
publisherId: 1234,
|
||||
}
|
||||
|
||||
const mutation = gql`
|
||||
mutation (
|
||||
$email: String!
|
||||
$firstName: String!
|
||||
$lastName: String!
|
||||
$language: String!
|
||||
$publisherId: Int
|
||||
) {
|
||||
createUser(
|
||||
email: $email
|
||||
firstName: $firstName
|
||||
lastName: $lastName
|
||||
language: $language
|
||||
publisherId: $publisherId
|
||||
)
|
||||
}
|
||||
`
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let newUser: User
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await mutate({ mutation, variables })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await resetDB()
|
||||
})
|
||||
|
||||
it('returns success', () => {
|
||||
expect(result).toEqual(expect.objectContaining({ data: { createUser: 'success' } }))
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
let loginUser: LoginUser[]
|
||||
let user: User[]
|
||||
let loginUserBackup: LoginUserBackup[]
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
beforeAll(async () => {
|
||||
loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
|
||||
user = await getRepository(User).createQueryBuilder('state_user').getMany()
|
||||
loginUserBackup = await getRepository(LoginUserBackup)
|
||||
.createQueryBuilder('login_user_backup')
|
||||
.getMany()
|
||||
loginEmailOptIn = await getRepository(LoginEmailOptIn)
|
||||
.createQueryBuilder('login_email_optin')
|
||||
.getMany()
|
||||
newUser = user[0]
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
})
|
||||
|
||||
describe('filling all tables', () => {
|
||||
it('saves the user in login_user table', () => {
|
||||
expect(loginUser).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
email: 'peter@lustig.de',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
username: '',
|
||||
description: '',
|
||||
password: '0',
|
||||
pubKey: null,
|
||||
privKey: null,
|
||||
emailHash: expect.any(Buffer),
|
||||
createdAt: expect.any(Date),
|
||||
emailChecked: false,
|
||||
passphraseShown: false,
|
||||
language: 'de',
|
||||
disabled: false,
|
||||
groupId: 1,
|
||||
publisherId: 1234,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('saves the user in state_user table', () => {
|
||||
expect(user).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
indexId: 0,
|
||||
groupId: 0,
|
||||
pubkey: expect.any(Buffer),
|
||||
email: 'peter@lustig.de',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
username: '',
|
||||
disabled: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('saves the user in login_user_backup table', () => {
|
||||
expect(loginUserBackup).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
passphrase: expect.any(String),
|
||||
userId: loginUser[0].id,
|
||||
mnemonicType: 2,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('creates an email optin', () => {
|
||||
expect(loginEmailOptIn).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
userId: loginUser[0].id,
|
||||
verificationCode: expect.any(String),
|
||||
emailOptInTypeId: 1,
|
||||
createdAt: expect.any(Date),
|
||||
resendCount: 0,
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('account activation email', () => {
|
||||
it('sends an account activation email', () => {
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(/\$1/g, emailOptIn)
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: `${newUser.firstName} ${newUser.lastName} <${newUser.email}>`,
|
||||
subject: 'Gradido: E-Mail Überprüfung',
|
||||
text:
|
||||
expect.stringContaining(`Hallo ${newUser.firstName} ${newUser.lastName},`) &&
|
||||
expect.stringContaining(activationLink),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('email already exists', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User already exists.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unknown language', () => {
|
||||
it('sets "de" as default language', async () => {
|
||||
await mutate({
|
||||
mutation,
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
|
||||
})
|
||||
await expect(
|
||||
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
|
||||
).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
language: 'de',
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('no publisher id', () => {
|
||||
it('sets publisher id to null', async () => {
|
||||
await mutate({
|
||||
mutation,
|
||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
||||
})
|
||||
await expect(
|
||||
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
|
||||
).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
publisherId: null,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await resetDB(true)
|
||||
await con.close()
|
||||
})
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
import fs from 'fs'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||
import { getConnection, getCustomRepository, getRepository } from 'typeorm'
|
||||
import { getConnection, getCustomRepository, getRepository, QueryRunner } from 'typeorm'
|
||||
import CONFIG from '../../config'
|
||||
import { User } from '../model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
@ -26,6 +26,7 @@ import { signIn } from '../../apis/KlicktippController'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
|
||||
import { ROLE_ADMIN } from '../../auth/ROLES'
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
const EMAIL_OPT_IN_RESET_PASSWORD = 2
|
||||
const EMAIL_OPT_IN_REGISTER = 1
|
||||
@ -148,6 +149,66 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
||||
|
||||
return message
|
||||
}
|
||||
const createEmailOptIn = async (
|
||||
loginUserId: number,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<LoginEmailOptIn> => {
|
||||
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
|
||||
let emailOptIn = await loginEmailOptInRepository.findOne({
|
||||
userId: loginUserId,
|
||||
emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
|
||||
})
|
||||
if (emailOptIn) {
|
||||
const timeElapsed = Date.now() - new Date(emailOptIn.updatedAt).getTime()
|
||||
if (timeElapsed <= parseInt(CONFIG.RESEND_TIME.toString()) * 60 * 1000) {
|
||||
throw new Error(
|
||||
'email already sent less than ' + parseInt(CONFIG.RESEND_TIME.toString()) + ' minutes ago',
|
||||
)
|
||||
} else {
|
||||
emailOptIn.updatedAt = new Date()
|
||||
emailOptIn.resendCount++
|
||||
}
|
||||
} else {
|
||||
emailOptIn = new LoginEmailOptIn()
|
||||
emailOptIn.verificationCode = random(64)
|
||||
emailOptIn.userId = loginUserId
|
||||
emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
|
||||
}
|
||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error while saving emailOptIn', error)
|
||||
throw new Error('error saving email opt in')
|
||||
})
|
||||
return emailOptIn
|
||||
}
|
||||
|
||||
const getOptInCode = async (loginUser: LoginUser): Promise<LoginEmailOptIn> => {
|
||||
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
|
||||
let optInCode = await loginEmailOptInRepository.findOne({
|
||||
userId: loginUser.id,
|
||||
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
|
||||
})
|
||||
|
||||
// Check for 10 minute delay
|
||||
if (optInCode) {
|
||||
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
|
||||
if (timeElapsed <= parseInt(CONFIG.RESEND_TIME.toString()) * 60 * 1000) {
|
||||
throw new Error(
|
||||
'email already sent less than ' + parseInt(CONFIG.RESEND_TIME.toString()) + ' minutes ago',
|
||||
)
|
||||
} else {
|
||||
optInCode.updatedAt = new Date()
|
||||
optInCode.resendCount++
|
||||
}
|
||||
} else {
|
||||
optInCode = new LoginEmailOptIn()
|
||||
optInCode.verificationCode = random(64)
|
||||
optInCode.userId = loginUser.id
|
||||
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
|
||||
}
|
||||
await loginEmailOptInRepository.save(optInCode)
|
||||
return optInCode
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class UserResolver {
|
||||
@ -372,7 +433,7 @@ export class UserResolver {
|
||||
dbUser.lastName = lastName
|
||||
dbUser.username = username
|
||||
// TODO this field has no null allowed unlike the loginServer table
|
||||
dbUser.pubkey = Buffer.alloc(32, 0) // default to 0000...
|
||||
dbUser.pubkey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
|
||||
// dbUser.pubkey = keyPair[0]
|
||||
|
||||
await queryRunner.manager.save(dbUser).catch((er) => {
|
||||
@ -383,37 +444,18 @@ export class UserResolver {
|
||||
|
||||
// Store EmailOptIn in DB
|
||||
// TODO: this has duplicate code with sendResetPasswordEmail
|
||||
const emailOptIn = new LoginEmailOptIn()
|
||||
emailOptIn.userId = loginUserId
|
||||
emailOptIn.verificationCode = random(64)
|
||||
emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
|
||||
const emailOptIn = await createEmailOptIn(loginUserId, queryRunner)
|
||||
|
||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error while saving emailOptIn', error)
|
||||
throw new Error('error saving email opt in')
|
||||
})
|
||||
|
||||
// Send EMail to user
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/\$1/g,
|
||||
emailOptIn.verificationCode.toString(),
|
||||
)
|
||||
const emailSent = await sendEMail({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: `${firstName} ${lastName} <${email}>`,
|
||||
subject: 'Gradido: E-Mail Überprüfung',
|
||||
text: `Hallo ${firstName} ${lastName},
|
||||
|
||||
Deine EMail wurde soeben bei Gradido registriert.
|
||||
|
||||
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
|
||||
${activationLink}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
})
|
||||
const emailSent = await this.sendAccountActivationEmail(
|
||||
activationLink,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
)
|
||||
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
@ -430,43 +472,83 @@ export class UserResolver {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
private sendAccountActivationEmail(
|
||||
activationLink: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
email: string,
|
||||
): Promise<boolean> {
|
||||
return sendEMail({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: `${firstName} ${lastName} <${email}>`,
|
||||
subject: 'Gradido: E-Mail Überprüfung',
|
||||
text: `Hallo ${firstName} ${lastName},
|
||||
|
||||
Deine EMail wurde soeben bei Gradido registriert.
|
||||
|
||||
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
|
||||
${activationLink}
|
||||
oder kopiere den obigen Link in dein Browserfenster.
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
})
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email: email })
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
|
||||
try {
|
||||
const emailOptIn = await createEmailOptIn(loginUser.id, queryRunner)
|
||||
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/\$1/g,
|
||||
emailOptIn.verificationCode.toString(),
|
||||
)
|
||||
|
||||
const emailSent = await this.sendAccountActivationEmail(
|
||||
activationLink,
|
||||
loginUser.firstName,
|
||||
loginUser.lastName,
|
||||
email,
|
||||
)
|
||||
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Account confirmation link: ${activationLink}`)
|
||||
}
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw e
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
|
||||
@Query(() => Boolean)
|
||||
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
|
||||
// TODO: this has duplicate code with createUser
|
||||
// TODO: Moriz: I think we do not need this variable.
|
||||
let emailAlreadySend = false
|
||||
|
||||
const loginUserRepository = await getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email })
|
||||
|
||||
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
|
||||
let optInCode = await loginEmailOptInRepository.findOne({
|
||||
userId: loginUser.id,
|
||||
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
|
||||
})
|
||||
if (optInCode) {
|
||||
emailAlreadySend = true
|
||||
} else {
|
||||
optInCode = new LoginEmailOptIn()
|
||||
optInCode.verificationCode = random(64)
|
||||
optInCode.userId = loginUser.id
|
||||
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
|
||||
await loginEmailOptInRepository.save(optInCode)
|
||||
}
|
||||
const optInCode = await getOptInCode(loginUser)
|
||||
|
||||
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
|
||||
/\$1/g,
|
||||
optInCode.verificationCode.toString(),
|
||||
)
|
||||
|
||||
if (emailAlreadySend) {
|
||||
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
|
||||
if (timeElapsed <= 10 * 60 * 1000) {
|
||||
throw new Error('email already sent less than 10 minutes before')
|
||||
}
|
||||
}
|
||||
|
||||
const emailSent = await sendEMail({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: `${loginUser.firstName} ${loginUser.lastName} <${email}>`,
|
||||
|
||||
@ -29,7 +29,7 @@ import { elopageWebhook } from '../webhook/elopage'
|
||||
// TODO implement
|
||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||
|
||||
const DB_VERSION = '0005-admin_tables'
|
||||
const DB_VERSION = '0006-login_users_collation'
|
||||
|
||||
const createServer = async (context: any = serverContext): Promise<any> => {
|
||||
// open mysql connection
|
||||
|
||||
@ -9,7 +9,17 @@ export class UserTransactionRepository extends Repository<UserTransaction> {
|
||||
limit: number,
|
||||
offset: number,
|
||||
order: Order,
|
||||
onlyCreation?: boolean,
|
||||
): Promise<[UserTransaction[], number]> {
|
||||
if (onlyCreation) {
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
.andWhere('userTransaction.type = "creation"')
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.getManyAndCount()
|
||||
}
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
|
||||
@ -146,7 +146,6 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
firstName,
|
||||
lastName,
|
||||
publisherId: loginElopgaeBuy.publisherId,
|
||||
password: '123', // TODO remove
|
||||
})
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@ -47,7 +47,8 @@
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@entity/*": ["../database/entity/*"]
|
||||
"@entity/*": ["../database/entity/*"],
|
||||
"@dbTools/*": ["../database/src/*"]
|
||||
},
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
|
||||
@ -4,6 +4,5 @@ DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
MIGRATIONS_TABLE=migrations
|
||||
MIGRATIONS_DIRECTORY=./migrations/
|
||||
|
||||
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
// For production you need to put your env file in here.
|
||||
// Please copy the dist file from the root folder in here and rename it to .env
|
||||
@ -2,7 +2,7 @@ import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'ty
|
||||
import { Balance } from '../Balance'
|
||||
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('state_users')
|
||||
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
@ -16,16 +16,28 @@ export class User extends BaseEntity {
|
||||
@Column({ type: 'binary', length: 32, name: 'public_key' })
|
||||
pubkey: Buffer
|
||||
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
|
||||
email: string
|
||||
|
||||
@Column({ name: 'first_name', length: 255, nullable: true, default: null })
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({ name: 'last_name', length: 255, nullable: true, default: null })
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
|
||||
username: string
|
||||
|
||||
@Column()
|
||||
|
||||
@ -2,7 +2,7 @@ import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 't
|
||||
import { UserSetting } from './UserSetting'
|
||||
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('state_users')
|
||||
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
@ -16,16 +16,28 @@ export class User extends BaseEntity {
|
||||
@Column({ type: 'binary', length: 32, name: 'public_key' })
|
||||
pubkey: Buffer
|
||||
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
|
||||
email: string
|
||||
|
||||
@Column({ name: 'first_name', length: 255, nullable: true, default: null })
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({ name: 'last_name', length: 255, nullable: true, default: null })
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@Column({ length: 255, nullable: true, default: null })
|
||||
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
|
||||
username: string
|
||||
|
||||
@Column()
|
||||
|
||||
60
database/entity/0006-login_users_collation/LoginUser.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
|
||||
import { LoginUserBackup } from '../LoginUserBackup'
|
||||
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('login_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class LoginUser extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ length: 191, unique: true, collation: 'utf8mb4_unicode_ci' })
|
||||
email: string
|
||||
|
||||
@Column({ name: 'first_name', length: 150, collation: 'utf8mb4_unicode_ci' })
|
||||
firstName: string
|
||||
|
||||
@Column({ name: 'last_name', length: 255, default: '', collation: 'utf8mb4_unicode_ci' })
|
||||
lastName: string
|
||||
|
||||
@Column({ length: 255, default: '', collation: 'utf8mb4_unicode_ci' })
|
||||
username: string
|
||||
|
||||
@Column({ default: '', collation: 'utf8mb4_unicode_ci' })
|
||||
description: string
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({ name: 'pubkey', type: 'binary', length: 32, default: null, nullable: true })
|
||||
pubKey: Buffer
|
||||
|
||||
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||
privKey: Buffer
|
||||
|
||||
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||
emailHash: Buffer
|
||||
|
||||
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'email_checked', default: 0 })
|
||||
emailChecked: boolean
|
||||
|
||||
@Column({ name: 'passphrase_shown', default: 0 })
|
||||
passphraseShown: boolean
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci' })
|
||||
language: string
|
||||
|
||||
@Column({ default: 0 })
|
||||
disabled: boolean
|
||||
|
||||
@Column({ name: 'group_id', default: 0, unsigned: true })
|
||||
groupId: number
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@OneToOne(() => LoginUserBackup, (loginUserBackup) => loginUserBackup.loginUser)
|
||||
loginUserBackup: LoginUserBackup
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { LoginUser } from './0003-login_server_tables/LoginUser'
|
||||
export { LoginUser } from './0006-login_users_collation/LoginUser'
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
/* MIGRATION TO ADD USER SETTINGS
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
* the database can be setup already but also may not be.
|
||||
* Therefore you will find all `CREATE TABLE` statements with
|
||||
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
|
||||
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
|
||||
* This ensures compatibility for existing or non-existing
|
||||
* databases.
|
||||
* This migration adds the table `user_setting` in order to store all sorts of user configuration data
|
||||
*/
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
/* MIGRATION TO CREATE THE LOGIN_SERVER TABLES
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
* the database can be setup already but also may not be.
|
||||
* Therefore you will find all `CREATE TABLE` statements with
|
||||
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
|
||||
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
|
||||
* This ensures compatibility for existing or non-existing
|
||||
* databases.
|
||||
* This migration creates the `login_server` tables in the `community_server` database (`gradido_community`).
|
||||
* This is done to keep all data in the same place and is to be understood in conjunction with the next migration
|
||||
* `0004-login_server_data` which will fill the tables with the existing data
|
||||
*/
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* FIRST MIGRATION
|
||||
/* MIGRATION TO COPY LOGIN_SERVER DATA
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
* the database can be setup already but also may not be.
|
||||
* Therefore you will find all `CREATE TABLE` statements with
|
||||
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
|
||||
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
|
||||
* This ensures compatibility for existing or non-existing
|
||||
* databases.
|
||||
* This migration copies all existing data from the `login_server` database (`gradido_login`)
|
||||
* to the `community_server` database (`gradido_community`) in case the login_server database
|
||||
* is present.
|
||||
*
|
||||
* NOTE: This will fail if the two databases are located on different servers.
|
||||
* Manual export and import of the database will be required then.
|
||||
* NOTE: This migration does not delete the data when downgrading!
|
||||
*/
|
||||
|
||||
const LOGIN_SERVER_DB = 'gradido_login'
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
/* MIGRATION FOR ADMIN INTERFACE
|
||||
*
|
||||
* This migration is special since it takes into account that
|
||||
* the database can be setup already but also may not be.
|
||||
* Therefore you will find all `CREATE TABLE` statements with
|
||||
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
|
||||
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
|
||||
* This ensures compatibility for existing or non-existing
|
||||
* databases.
|
||||
* This migration adds the table `login_pending_tasks_admin` to store pending creations
|
||||
*/
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
|
||||
16
database/migrations/0006-login_users_collation.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/* MIGRATION TO ALIGN COLLATIONS
|
||||
*
|
||||
* in oder to be able to compare `login_users` with `state_users`
|
||||
* when the databases default is not `utf8mb4_unicode_ci`, we need
|
||||
* to also explicitly define it in the table
|
||||
*/
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;',
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `login_users` CONVERT TO CHARACTER SET utf8mb4;')
|
||||
}
|
||||
@ -10,9 +10,9 @@
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "tsc --build --clean",
|
||||
"up": "cd build && node src/index.js up",
|
||||
"down": "cd build && node src/index.js down",
|
||||
"reset": "cd build && node src/index.js reset",
|
||||
"up": "node build/src/index.js up",
|
||||
"down": "node build/src/index.js down",
|
||||
"reset": "node build/src/index.js reset",
|
||||
"dev_up": "ts-node src/index.ts up",
|
||||
"dev_down": "ts-node src/index.ts down",
|
||||
"dev_reset": "ts-node src/index.ts reset",
|
||||
|
||||
@ -13,7 +13,6 @@ const database = {
|
||||
|
||||
const migrations = {
|
||||
MIGRATIONS_TABLE: process.env.MIGRATIONS_TABLE || 'migrations',
|
||||
MIGRATIONS_DIRECTORY: process.env.MIGRATIONS_DIRECTORY || './migrations/',
|
||||
}
|
||||
|
||||
const CONFIG = { ...database, ...migrations }
|
||||
|
||||
34
database/src/helpers.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import CONFIG from './config'
|
||||
import { createPool, PoolConfig } from 'mysql'
|
||||
import { Migration } from 'ts-mysql-migrate'
|
||||
import path from 'path'
|
||||
|
||||
const poolConfig: PoolConfig = {
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
user: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
}
|
||||
|
||||
// Pool?
|
||||
const pool = createPool(poolConfig)
|
||||
|
||||
// Create & Initialize Migrations
|
||||
const migration = new Migration({
|
||||
conn: pool,
|
||||
tableName: CONFIG.MIGRATIONS_TABLE,
|
||||
silent: true,
|
||||
dir: path.join(__dirname, '..', 'migrations'),
|
||||
})
|
||||
|
||||
const initialize = async (): Promise<void> => {
|
||||
await migration.initialize()
|
||||
}
|
||||
|
||||
const resetDB = async (closePool = false): Promise<void> => {
|
||||
await migration.reset() // use for resetting database
|
||||
if (closePool) pool.end()
|
||||
}
|
||||
|
||||
export { resetDB, pool, migration, initialize }
|
||||
@ -1,7 +1,4 @@
|
||||
import 'reflect-metadata'
|
||||
import { createPool, PoolConfig } from 'mysql'
|
||||
import { Migration } from 'ts-mysql-migrate'
|
||||
import CONFIG from './config'
|
||||
import prepare from './prepare'
|
||||
import connection from './typeorm/connection'
|
||||
import { useSeeding, runSeeder } from 'typeorm-seeding'
|
||||
@ -10,30 +7,12 @@ import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed'
|
||||
import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed'
|
||||
import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed'
|
||||
import { DecayStartBlockSeed } from './seeds/decay-start-block.seed'
|
||||
import { resetDB, pool, migration } from './helpers'
|
||||
|
||||
const run = async (command: string) => {
|
||||
// Database actions not supported by our migration library
|
||||
await prepare()
|
||||
|
||||
// Database connection for Migrations
|
||||
const poolConfig: PoolConfig = {
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
user: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
}
|
||||
|
||||
// Pool?
|
||||
const pool = createPool(poolConfig)
|
||||
|
||||
// Create & Initialize Migrations
|
||||
const migration = new Migration({
|
||||
conn: pool,
|
||||
tableName: CONFIG.MIGRATIONS_TABLE,
|
||||
dir: CONFIG.MIGRATIONS_DIRECTORY,
|
||||
})
|
||||
|
||||
// Database connection for TypeORM
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
@ -52,7 +31,7 @@ const run = async (command: string) => {
|
||||
break
|
||||
case 'reset':
|
||||
// TODO protect from production
|
||||
await migration.reset() // use for resetting database
|
||||
await resetDB() // use for resetting database
|
||||
break
|
||||
case 'seed':
|
||||
// TODO protect from production
|
||||
|
||||
@ -2,25 +2,39 @@
|
||||
|
||||
Benutzer
|
||||
|
||||
* *
|
||||
* natürliche Person = TRUE (Flag für Schöpfungserlaubnis Human)
|
||||
* Vorname
|
||||
* Nachname
|
||||
* natürliche Person = FALSE (Flag für Schöpfungserlaubnis Human)
|
||||
* Projekt/Firmenname/Organisation/Verein...
|
||||
* Benutzername
|
||||
* Emailadresse
|
||||
* Konto
|
||||
* Trustlevel (Zukunft)
|
||||
* natürliche Person = TRUE (Flag für Schöpfungserlaubnis Human)
|
||||
|
||||
* Vorname
|
||||
* Nachname
|
||||
* natürliche Person = FALSE (Flag für Schöpfungserlaubnis Human)
|
||||
|
||||
Gradido-ID als Ersatz für Email-Adresse : server/nutzername
|
||||
* Projekt/Firmenname/Organisation/Verein...
|
||||
* Benutzername / useralias
|
||||
* Emailadresse
|
||||
* Konto
|
||||
* Trustlevel (Zukunft)
|
||||
|
||||
## Gradido-ID
|
||||
|
||||
Die GradidoID oder auch Username dient zur eindeutigen Identifizierung eines Nutzers. Sie soll einerseits *human readable* , aber auch die maschinelle Weiterverarbeitung bzw. Nutzung ermöglichen. Daher werden folgende Regeln festgelegt:
|
||||
|
||||
Pattern: `<communityname>`/`<useralias>`
|
||||
|
||||
* `<communityname>`
|
||||
- Zeichen, die in einer URL erlaubt sind:
|
||||
* A-Z, a-z, 0-9
|
||||
* '-', '.', '_', '~'
|
||||
* `<useralias>`:
|
||||
- mindestens 5 Zeichen
|
||||
* alphanumerisch
|
||||
* keine Umlaute
|
||||
* - und \_ sind nur zwischen sonst gültigen Zeichen erlaubt (RegEx: [a-zA-Z0-9]-|_[a-zA-Z0-9])
|
||||
- Blacklist für Schlüsselworte, die frei definiert werden können
|
||||
- vordefinierte/reservierte System relevante Namen dürfen maximal aus 4 Zeichen bestehen
|
||||
|
||||
Nutzername ist pro server eindeutig
|
||||
|
||||
Nutzerprofil mit Bild und persönlichen Angeboten
|
||||
|
||||
|
||||
## Anwendungsfälle
|
||||
|
||||
### neuen Benutzer anlegen
|
||||
|
||||
137
docu/Concepts/BusinessRequirements/Betrieb und Support.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Betrieb und Support
|
||||
|
||||
Dieses Dokument beschreibt die Anforderungen für den Betrieb und Support der Gradido-Anwendung während der Entwicklungsphase. Diese gelten nicht in vollem Umfang für den Betrieb der Gradido-Anwendung für eine neue Community, die sich rein auf eine Produktiv-Umgebung reduzieren läßt.
|
||||
|
||||
# Umgebungen
|
||||
|
||||
Für die Phase der Entwicklung werden folgende Stages benötigt:
|
||||
|
||||
* LOKAL-Stage: Entwicklungsumgebung für einen Entwickler
|
||||
* DEV-Stage: zentrale Entwicklungsumgebung für erste Integrationsschritte aus dem Entwicklerteam
|
||||
* INT-Stage: zentrale, produktionsnahe Integrationsumgebung zum Aufbau und Test eines neuen Release
|
||||
* PROD-Stage: zentrale Produktiv-Umgebung für den Betrieb mindestens einer Community
|
||||
|
||||
|
||||
## LOKAL-Stage
|
||||
|
||||
Die lokale Entwicklungsumgebung liegt in dem Verantwortungsbereich eines jeden Entwicklers. Es müssen dabei für die Entwicklung im Gradido-Team alle notwendigen Tools und Komponenten installiert bzw. vorhanden sein.
|
||||
|
||||
### Resources
|
||||
|
||||
### Betriebesystem
|
||||
|
||||
### Tools
|
||||
|
||||
### Komponenten und Dienste
|
||||
|
||||
|
||||
|
||||
## DEV-Stage
|
||||
|
||||
|
||||
### Resources
|
||||
|
||||
CPU
|
||||
|
||||
RAM
|
||||
|
||||
Storage
|
||||
|
||||
Network/Traffic
|
||||
|
||||
Backup
|
||||
|
||||
### Betriebesystem
|
||||
|
||||
### Tools
|
||||
|
||||
### Komponenten und Dienste
|
||||
|
||||
|
||||
## INT-Stage
|
||||
|
||||
|
||||
### Resources
|
||||
|
||||
CPU
|
||||
|
||||
RAM
|
||||
|
||||
Storage
|
||||
|
||||
Network/Traffic
|
||||
|
||||
Backup
|
||||
|
||||
### Betriebesystem
|
||||
|
||||
### Tools
|
||||
|
||||
### Komponenten und Dienste
|
||||
|
||||
|
||||
## PROD-Stage
|
||||
|
||||
|
||||
### Resources
|
||||
|
||||
CPU
|
||||
|
||||
RAM
|
||||
|
||||
Storage
|
||||
|
||||
Network/Traffic
|
||||
|
||||
Backup
|
||||
|
||||
|
||||
### Betriebesystem
|
||||
|
||||
### Tools
|
||||
|
||||
### Komponenten und Dienste
|
||||
|
||||
#### Security
|
||||
|
||||
#### Backup und Restore
|
||||
|
||||
tägliches Backup
|
||||
|
||||
#### Redundanz und Failover
|
||||
|
||||
Virtulisierung
|
||||
|
||||
Balancing
|
||||
|
||||
|
||||
|
||||
# Provider
|
||||
|
||||
Für das Hosting der Stages DEV, INT, PROD wird ein Provider gesucht, der die gewünschten Anforderungen als Dienstleister zu einem angemessenen Preis und mit seriösem Hintergrund anbietet. Es sollen hier verschiedenen Provider aufgelistet und ihre Leistungen und Kosten gegenübergestellt werden, so dass eine Entscheidungsfindung für eine Beauftragung möglich wird.
|
||||
|
||||
## aktuell beauftragte Provider und Dienstleister
|
||||
|
||||
Derzeit (10.2021) sind Leistungen und Dienste von folgenden Providern gebucht:
|
||||
|
||||
### Elopage
|
||||
|
||||
Forum, Mitglieder-Pakete, Spenden
|
||||
|
||||
### Klicktipp
|
||||
|
||||
Newsletter
|
||||
|
||||
### ebiz
|
||||
|
||||
Marktplatz
|
||||
|
||||
### 1fire
|
||||
|
||||
Webhoster PROD
|
||||
|
||||
ohne Backup
|
||||
|
||||
### strato
|
||||
|
||||
Webhoster DEV und INT
|
||||
@ -4,66 +4,119 @@ Diese Konzept beschreibt den Begriff "Community" im Kontext von Gradido, welche
|
||||
|
||||
## Die Bedeutung des Begriffs Community
|
||||
|
||||
Eine Community bedeutet im Kontext von Gradido eine Gemeinschaft von Personen, die sich nach der Philosophie von Gradido zu einer gemeinsamen Gruppierung zusammenschließen. Unter dem gemeinsamen Zusammenschluß folgen sie der Natürlichen Ökonomie des Lebens. Die Community dient dabei als Rahmen für die Gruppe von Personen, um ihnen den geregelten Zugang zu ermöglichen. Unter dem Begriff "Zugang zur Community" wird die Registrierung eines Benutzerkontos für eine Person verstanden. Dabei erfolgt eine Autentifizierung der Person, um einen personenspezifischen Zugriff auf die Community-Funktionalitäten zu ermöglichen. Denn eine Community bietet einer Person eine Vielzahl an Funktionalitäten, die ein Community-Mitglied nutzen kann. So steht die Verwaltung und das Handeln mit Gradido-Geld als die Hauptfunktionalität einer Community im Vordergrund. Doch sind auch weitere Funktionalitäten, wie eine Selbstdarstellung über Benutzerprofile oder ein sich Vernetzen mit Community-Mitgliedern, aber auch ein Community übergreifendes Vernetzen als soziale Netzewerke möglich. So können aus kleinen Communities über Vertrauensverhältnisse Zusammenschlüsse mehrere eigenständigen Communities entstehen oder auch eine Hierarchie von Communities als Parent-Child-Verbindung aufgebaut werden (siehe weiter unten "Community-Modelle").
|
||||
Eine Community bedeutet im Kontext von Gradido eine Gemeinschaft von Personen, die sich nach der Philosophie von Gradido zu einer gemeinsamen Gruppierung zusammenschließen. Unter dem gemeinsamen Zusammenschluß folgen sie also einerseits gemäß der Gradido-Philosophie der *Natürlichen Ökonomie des Lebens* und andererseits ihrer ursprünglichen Idee eine Gemeinschaft zu bilden.
|
||||
|
||||
Innerhalb der Community erfolgt die Umsetzung und Verwaltung des "lebendigen Geldes". Soll heißen hier werden die Mechanismen zur Dreifachen-Schöpfung vollzogen, die das geschöpfte Geld nach den Community-Regeln auf die drei Arten von Empfängerkonten (Benutzerkonto, Gemeinwohlkonto und Ausgleichs- und Umweltkonto) verteilt. Ein Community-Mitglied kann über seinen Community-Zugang auf sein persönliches Benutzerkonto zugreifen und darüber sein Gradido-Geld verwalten. Neben der Einsicht auf seinen aktuellen Kontostand kann er u.a. seine regelmäßig geschöpften Gradido einsehen, mit vorhandenen Gradido bezahlen oder einem anderen Mitglied Gradido überweisen. Die Geldbewegungen werden als eine Liste von Transaktionen geführt und die Vergänglichkeit der Gradidos immer aktuell zur Anzeige gebracht.
|
||||
Als Gradido System-Komponente beinhaltet die *Community* die grundlegenden Funktionalitäten und Prozesse zur Verwaltung der Gruppenmitglieder, ihrer Registrierungs- und Systemzugriffe, die Konten- und Geldverwaltung einerseits und andererseits die Funktionalitäten und Prozesse zur Vernetzung und Kommunikation von mehreren Communities untereinander .
|
||||
|
||||
Innerhalb der Community-Komponente erfolgt die Umsetzung und Verwaltung des *lebendigen Geldes*. Soll heißen hier werden die Mechanismen zur Dreifachen-Schöpfung vollzogen, die das geschöpfte Geld nach den Community-Regeln auf die drei Arten von Empfängerkonten (AktivesGrundeinkommenkonto, Gemeinwohlkonto und Ausgleichs- und Umweltkonto) verteilt. Ein Community-Mitglied kann über seinen Community-Zugang auf sein persönliches Benutzerkonto zugreifen und darüber sein Gradido-Geld verwalten. Neben der Einsicht auf seinen aktuellen Kontostand kann er u.a. seine regelmäßig geschöpften Gradido einsehen, mit vorhandenen Gradido bezahlen oder einem anderen Mitglied Gradido überweisen. Die Geldbewegungen werden als eine Liste von Transaktionen geführt und die Vergänglichkeit der Gradidos immer aktuell zur Anzeige gebracht.
|
||||
|
||||
Nach der Bedeutung des Begriffs Community werden nun die Eigenschaften einer Community detailliert beschrieben, damit all die zuvor erwähnten Möglichkeiten der Community abbildbar sind.
|
||||
|
||||
## Eigenschaften einer Community
|
||||
|
||||
Hier werden die Eigenschaften einer Community beschrieben, die notwendig sind, um die oben erwähnten Möglichkeiten der Community zu erfüllen. Es geht dabei um verschiedene Themen und ihre dazu notwendigen Prozesse, die wiederum unter Verweiß in anderen Dokumenten detailter beschrieben sind.
|
||||
Hier werden die Eigenschaften einer Community beschrieben, die notwendig sind, um die oben erwähnten Möglichkeiten der Komponente zu erfüllen. Es geht dabei um verschiedene Themen und ihre dazu notwendigen Prozesse, die wiederum unter Verweiß in anderen Dokumenten detailter beschrieben sind.
|
||||
|
||||
### Währung
|
||||
|
||||
In einer Community werden die Prozesse der 3-fachen-Geldschöpfung, sowie der Transfer von Geld in der *Gradido-Währung* ablaufen. Mit dem Erstellen einer neuen Community wird technisch gesehen gleichzeitig auch eine eigene *Community-Gradido* Währung bei der Schöpfung erzeugt.
|
||||
|
||||
Ziel dieser Community eigenen Währung ist für die Gemeinschaft über ein Währungs-Branding sich marketingstechnisch hervorheben zu können. Zum Beispiel könnte eine Community aus der Region "Liebliches Taubertal" sich über den *Community-Gradido* den sogenannten "Taubertäler" erzeugen, den die Mitglieder dann aber auch überregional mit anderen Communities in Umlauf bringen und somit Werbung für ihre Region machen.
|
||||
|
||||
| PR-Kommentare | |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Was hälst du von einem Namen für die Währung "Der Taubertäler", den jede Community definieren kann, sich aber auch zu einer anderen anschließen kann. Dies ist aber nur das Branding in Sachen Anzeige & Marketing. Darunter liegt der Gradido - dieser ist weiter aufgestellt als nur regional - nämlich weltweit. Auch hier müssen "Bösewichte" ausgeschlossen werden können - Prozess & Regeln dafür?<br /><br />Ich mache mir einfach ein wenig Sorgen, dass wir die Anforderung "Jeder Coin hat eine eindeutige Community-Prägung" nicht erfüllen können mit der aktuellen Implementierung und ich habe auch Schwierigkeiten mir vorzustellen, wie sich das mit Schwundgeld verhalten soll - insbesondere mit stärker schwindendem Geld bei größerer Geldmenge. Der Schwund ist also auf alle meine Coins gebunden, währen die Prägung auf den jeweiligen Coin passiert. Ich glaube das ist sehr schwierig beides entsprechend abzubilden. |
|
||||
| Claus-Peter 25.11.2021 | Ich kann die Bedenken einerseits verstehen und andererseits die Visionen von Bernd nachvollziehen. Dass diese nicht immer einfach unter einen Hut zu bringen sind, ist klar und dennoch möchte ich diese zumindest in den fachlichen Anforderungen nicht einfach rausstreichen.<br />Wie diese dann technisch zu konzipieren sind ist und bleibt letztendlich genau unsere Aufgabe. |
|
||||
|
||||
Andererseits soll aber, wenn eine Community sich bei der Geldschöpfung nicht an die Regel der *Gradido-Philosopie* hält, eine technische Möglichkeit geschaffen sein, dass diese Community in ihrer weiteren Geldschöpfung und dem Handel *ihrer* Währung sanktioniert werden kann.
|
||||
|
||||
Aber grundsätzlich bleibt bei allen *Community-Gradido*-Währungen die Vergänglichkeit als Sicherungsmechanismus des Geldvolumens und der 1:1 Umtausch zwischen verschiedenen *Community-Gradidos* bestehen.
|
||||
|
||||
#### Schutz vor Falschgeld
|
||||
|
||||
- Blacklisting
|
||||
- Bereinigung durch Bezahlen nach Priorisierung
|
||||
- 1. GDD von der Community des Empfängers
|
||||
2. GDD von anderen Communities nach Menge von wenig nach viel
|
||||
3. GDD von der eigenen Community
|
||||
4. geblacklistete werden gar nicht verwendet und vergehen
|
||||
|
||||
* Vergänglichkeitsbereinigung
|
||||
* 1. GDD anderer Communities nach Menge von wenig nach viel
|
||||
|
||||
|
||||
| PR-Kommentare | |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Wenn die geblacklisteten Coins prioritisiert vergehen kann der findige Angreifer den Verfall umgehen, indem er sich ungültige/blacklistete Coins erschafft, die dann genau seinem Verfall entsprechen. |
|
||||
| Claus-Peter 25.11.2021 | Das Kapitel "Schutz vor Falschgeld" ist wohl eher noch im Status "Brainstorming" zu verstehen. Hier wurden mögliche Regeln notiert, die noch nicht in ihrer Gänze durchdacht und konzipiert sind.<br />Der Punkt Vergänglichkeitsbereinigung sagt folgendes aus:<br />Kontostand am 01.01.2021:<br />- 100 GDD aus der eigenen Community<br />- 100 GDD aus Community A<br />- 100 GDD aus Community B<br />- gesamt 300 GDD<br /><br />Nach einem Jahr ohne irgendwelche weiteren Transaktionen ergibt sich folgendes am 31.12.2021:<br />- Vergänglichkeit mit 365 Tagen bei 300 GDD = 150 GDD (nicht gerechnet, sondern 50%)<br />- die 150 GDD Vergänglichkeit als Tx-Buchung führt zu folgendem Kontostand:<br /> * 100 GDD aus der eigenen Community<br /> * 50 GDD aus der Community A<br /> * 0 GDD aus der Community B<br /> * gesamt 150 GDD<br /><br />Soweit der Gedankengang zur Bereinigung des Kontos mit GDD aus anderen Communities. Das hat keine Auswirkung auf die Wertigkeit, sondern soll sich allein auf die Reduktion der Viefältigkeit an Community-Währungen im eigenen Konto führen. |
|
||||
|
||||
|
||||
* Bezahl-Vorbereitung
|
||||
* Austausch von Blacklist zw. Teilnehmer
|
||||
* ggf. Übersteuern der Balcklist falls gewünscht
|
||||
|
||||
| PR-Kommentare | |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Ich denke die vervielfachung von Coins und damit eine Ab/Auf-Wertung der jeweiligen Währen einer Community ist entgegen dem Konzept von Gradido. Gradido schafft eine stabile Zeit-Tausch-Einheit. Diese sollte Weltweit den gleichen Wert haben - warum sollte der Peruaner für seine Zeit weniger Gradido bekommen als ein Europäer? Das zementiert einfach weiterhin die bestehende Ordnung auf dem Planeten. Wollen wir das?<br />Daher mein Credo: Niemals einen Faktor zwischen Communities einführen. |
|
||||
| Claus-Peter 25.11.2021 | Den Kommentar verstehe ich nicht in Bezug auf die zitierten Dokument-Zeilen.<br />Wo finden sich Hinweise auf eine Ab/Auf-Wertung der Währung?<br />Das Blacklisting ist mit Sicherheit ein sehr sensibler Punkt und muss genauestens und tiefer durchdacht und konzipiert werden.<br />Ansonsten stimme ich dem Inhalt und deinem Credo voll zu. |
|
||||
|
||||
|
||||
### Anzeige und -Darstellung
|
||||
|
||||
Da es also mehrere Communities geben wird, benötigt jede Community ihren eigenen Namen und gar ein Symbol oder Bild, um eine optische Unterscheidung bei der Anzeige in den Systemen sicherzustellen. Für eine Aussendarstellung wäre eine Beschreibung der Community und ihre eigene Philosopie, was die Community auszeichnet hilfreich. Diese Werte müssen vom Community-Administrator gepflegt werden können.
|
||||
Da es also mehrere Communities geben wird, benötigt jede Community ihren eigenen Namen und gar ein Symbol oder Bild, um eine optische Unterscheidung oder gar eigenes Branding bei der Anzeige in den Systemen sicherzustellen. Für eine Aussendarstellung wäre eine Beschreibung der Community und ihre eigene Philosopie, was die Community auszeichnet hilfreich. Diese Werte müssen vom Community-Administrator gepflegt werden können.
|
||||
|
||||
### Mitgliederverwaltung
|
||||
|
||||
Für die Verwaltung von Community-Mitgliedern werden entsprechende Verwaltungsprozesse wie Registrierung, Login mit Autentifizierung, eine Benutzerverwaltung für neue, bestehende und ausscheidende Mitgleider benötigt. Die Benutzerverwaltung stellt zusätzlich die Anforderung, dass ein Community-Mitglied eindeutig identifizierbar ist und das Community übergreifend. Das bedeutet es kann eine Person immer nur einmal existieren und darf auch niemals in mehreren Communities gleichzeitig Mitglied sein. Denn es muss sichergestellt werden, dass eine Person sich keine unerlaubte Vorteil durch zum Beispiel mehrfache Geldschöpfung in mehreren Communities verschafft. Die Details der Mitgliederverwaltung werden beschrieben im Dokument [BenutzerVerwaltung](.\BenutzerVerwaltung.md).
|
||||
Für die Verwaltung von Community-Mitgliedern werden entsprechende Verwaltungsprozesse wie Registrierung, Login mit Autentifizierung, eine Benutzerverwaltung für neue, bestehende und ausscheidende Mitgleider benötigt. Die Benutzerverwaltung stellt zusätzlich die Anforderung, dass ein Community-Mitglied eindeutig identifizierbar ist und das Community übergreifend. Das bedeutet es kann eine Person immer nur einmal existieren und darf auch niemals in mehreren Communities gleichzeitig Mitglied sein. Denn es muss sichergestellt werden, dass eine Person sich keine unerlaubte Vorteile durch zum Beispiel mehrfache Geldschöpfung in mehreren Communities verschafft.
|
||||
|
||||
### Community-Vernetzung
|
||||
Die Details der Mitgliederverwaltung werden beschrieben im Dokument [BenutzerVerwaltung](.\BenutzerVerwaltung.md).
|
||||
|
||||
Für die Community-Vernetzung sind Verwaltungsprozesse zwischen den Communities und auch den Community-Mitgliedern notwendig, um entsprechende Vertrauensverhältnisse aufzubauen. Diese müssen den notwendigen Sicherheitsansprüchen genügen, da darauf aufbauend dann später die Geld-Flüsse abgewickelt werden. Entsprechend den Community-Modellen (siehe im folgenden Unterkapitel **Community Modelle**) wird ein Prozess benötigt, der die Hierarchie bzw. das Vertrauensverhältnis zwischen zwei eigenständigen Communities aufbaut und daraus dann die möglichen Funktionalitätserweiterungen für die Mitglieder bzw. den Communities freischaltet bzw. unterstützt. Zusätzlich wird auch der jeweilige umgekehrte Prozess benötigt, der eine bestehende Hierarchie bzw. ein bestehendes Vertrauensverhältnis zwischen zwei Communities auflöst und löscht, sowie die daraus resultierenden Funktionseinschränkungen für die Mitglieder und die betroffenen Communities.
|
||||
### Community-Netzwerk
|
||||
|
||||
Zum besseren Verständnis der Community-Vernetzung erfolgt hier eine Beschreibung der möglichen Konstellationen, wie sich Communities miteinander verbinden können.
|
||||
Ein grundlegender Ansatz des Gradido-Systems beinhaltet die Einstufung aller beteiligten Gradido-Communities als gleichberechtigte Einheiten. Diese bilden unterneinander ein Kommunikations-Netzwerk zum Austausch an Informationen, aber auch zum Aufbau eines gemeinsamen Verbundes weiterer Aktivitäten.
|
||||
|
||||
#### Community Modelle
|
||||
#### Vernetzung
|
||||
|
||||
Bei Gradido werden verschiedene Modelle von Community-Abhängigkeiten unterstützt. Dabei soll unterschieden werden zwischen:
|
||||
Die Vernetzung der Gradido-Communities erfolgt automatisch über eine Channel-Infrastruktur.
|
||||
|
||||
* eigenständige Community
|
||||
* sich gegenseitig vertrauende Communities
|
||||
* von einander abhängige (vererbende) Communities
|
||||
* Mischung aus den vorherigen Modellen
|
||||

|
||||
|
||||
Das nachfolgende Bild zeigt einen ersten Eindruck über die unterschiedlichen Community-Modelle:
|
||||
Das bedeutet mit dem Aufsetzen und Inbetriebnehmen einer neuen Community erfolgt eine automatisierte Vernetzung der neuen Community mit den schon existierenden Communities über einen dedizierten Kommunikationskanal. Dies dient in aller erster Linie dazu, dass sich alle Gradido-Communities untereinander kennen lernen. Das dadurch entstehende Netzwerk von Gradido-Communities benötigt somit keinen zentralen Knoten, der die Verwaltung der dem Netzwerk beigetretenen Instanzen übernehmen müsste.
|
||||
|
||||

|
||||

|
||||
|
||||
##### Eigenständige Community
|
||||
Alle späteren Aktivitäten wie u.a. das gemeinsame Handeln oder Gradido-Transfer erfolgen dann in direkter Kommunikation zwischen zwei Communities. Dabei lauschen die Communities an nach fachlichen Themen separierte Kommunikationskanäle. Sobald eine direkt an eine Community adressierte oder auch wenn eine für eine Community interessante Nachricht empfangen wird, erfolgt die weitere Verarbeitung dieser Nachricht in direktem Austausch der beiden betroffenen Communities. Durch die Teilnahme einer Community an spezifischen fachlichen Kommunikationskanäle lernen sich die Communities untereinander an ihren spezifischen Interessen besser kennen bzw. können auch durch aktives Propagieren die engere Vernetzung zwischen den Communities beschleunigen.
|
||||
|
||||
Eine eigenständige Community zeichnet sich darin aus, dass sie keine Beziehung zu einer anderen Community aufgebaut hat. Das heißt sie hat weder eine vertrauenswürdige Verknüpfung mit einer zweiten Community, noch hat sie eine Verbindung zu einer Parent-Community und besitzt auch selbst keine Verbindung zu einer Child-Community. Somit kann diese Community für ihre Mitglieder nur Community intern wirksame Prozesse anbieten. Das heißt es ist kein Community übergreifender Handel bzw. Austausch von Gradido möglich. Andererseits werden in dieser Community die Prozesse freigeschaltet, dass ein Aufbau eines vertrauenswürdiges Verhältnis zu einer anderen Community erlaubt, der Aufbau einer Parent-Beziehung und auch der Aufbau einer Child-Beziehung ermöglicht. Die zugehörigen Abbau-Prozesse dagegen sind nicht freigeschalten. Der Community übegreifende Überprüfungsprozess bei der Mitglieder-Registrierung zur eindeutigen Identifikation in der Mitglieder-Verwaltung zählt dabei nicht als vertrauenswürdige Verbindung zwischen Communities.
|
||||

|
||||
|
||||
##### Gegenseitig vertrauende Communities
|
||||
#### Ausfallsicherheit
|
||||
|
||||
*Hier soll beschrieben werden, was den Unterschied auszeichnet zu einer "Eigenständigen Community", wie man das gegenseitige Vertrauen (sprich Verknüpfung) zwischen zwei oder mehreren Communities auf- und wieder abbaut, was bedarf es an Vorraussetzungen für einen Vertrauens-Auf/Abbau und welche Konsequenzen der Auf- und Abbau des gegenseitigen Vertrauens haben soll.*
|
||||
Ein weiterer wichtiger Aspekt der Community-Vernetzung ist die Sicherstellung der Ausfallsicherheit. Dabei erfolgt im Community-Netzwerk die Verteilung von Community eigenen Daten auf Knoten anderer Communities. Dadurch kann jederzeit bei einem Ausfall eines Netzwerkknotens und den damit betroffenen Communities einerseits ein online Fail-Over-Szenario betrieben werden und/oder andererseits der Wiederaufbau eines neuen Knotens mit den verlorenen Community-Daten und aus dem Netzwerk wiederhergestellten Daten erfolgen.
|
||||
|
||||
Das Modell der sich *gegenseitig vertrauenden Communities* entspringt der Idee des sich miteinander Vernetzens und damit das Handeln und Agieren mit Gradido-Mitgliedern, die nicht in der eigenen Community als Mitglied registriert sind. Um dies zu ermöglichen bedarf es einem Aufbau-Prozess zwischen zwei Communities, die sich zukünftig gegenseitig ein enges Vertrauen schenken. Auf der Basis dieses Vertrauens tauschen die beiden Communities Informationen untereinander aus, so dass für die Mitglieder beider Communities die Funktionalitäten auf der Gradido-Plattform so transparent erscheinen, als ob sie alle Mitglied einer Community wären. Das würde sich beispielsweise bei der Suche nach einem bestimmten Community-Mitglied auswirken, da nun alle Mitgleider beider Communities in einer Liste zur Anzeige gebracht werden können. Oder der Transfer von Gradidos von einem Mitglied zu einem anderen Mitglied ist über dieses Community-Verhältnis nun auch Community übergreifend möglich. Auch weitere Angebote, die bisher nur in einer Community zur Verfügung standen, sind nun auch den Mitgliedern der anderen Community zugänglich.
|
||||
#### Eindeutige Mitgliedschaft
|
||||
|
||||
Während des Aufbau-Prozesses werden neben den eigentlichen Security relevanten Informationen für den Aufbau und die Sicherstellung des Vertrauensverhältnisses auch fachliche Informationen ausgetauscht. Unter fachlichen Informationen sind die nun freigeschaltenen Angebote beider Communities gemeint. Somit werden in der einen Community nun auch die fachlichen Prozesse und Angebote der anderen Community zugänglich und freigeschalten und umgekehrt. Wie feingranular die Prozesse und Angebote dabei ausgetauscht und freigeschaltet werden unterliegt einer administrativen Konfiguration der jeweiligen Community. Das heißt der Administrator jeder Community kann im Vorfeld selektiv konfigurieren welche Angebote und Prozesse beim Aufbau-Prozess für ein Vertrauensverhältnis mit einer anderen Community übertragen und freigeschaltet werden. Diese Konfiguration sollte zuvor Community intern abgestimmt sein, um nicht schon zu Beginn der Zusammenarbeit der beiden Communities irgendwelche Missstimmungen unter den Mitgliedern zu verursachen. Die Details des *Vertrauensverhältnis Aufbau-Prozesses* sind weiter unten im Kapitel **Anwendungsfälle** beschrieben.
|
||||
Durch das Community-Netzwerk erfolgt auch der sehr wichtige Prozess der Sicherstellung, dass eine natürliche Person sich nur einmal bei einer Community im gesamten Community-Netzwerk registrieren darf. Dazu erfolgt ein Informationsaustausch über einen bestimmten Kommunikationskanal zwischen allen Communities untereinander. Das dazu notwendige Protokoll und die benötigten Daten werden im technischen Konzept definiert. Die Entscheidung, ob die Überprüfung der eindeutigen Mitgliedschaft direkt mit dem eigentlichen Registrierungsprozess eines Mitglieds gekoppelt werden kann oder ob diese nachträglich asynchron im Hintergrund stattfinden muss, findet erst bei der technischen Konzeption ggf. durch ein technisches Proof-of-Concept statt.
|
||||
|
||||
##### Abhängige Communities
|
||||
| PR-Kommentar | |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Diese Anforderung ist technisch nicht zu erfüllen.<br />1. Eine Identifikation einer Person erfolgt immer mithilfe eines technischem Identifiers wie z.B. der Personalausweisnummer, EMail, Telefonnummer oder anderes. Schon durch diese Abstraktion kann ein Nutzer mehr als eine Identität aufbauen (2 Telefonnummer z.B.)<br />2. Die Prüfung auf Eindeutigkeit in einem dezentralen Netzwerk kann nicht sichergestellt werden, da Teile des Netzwerks zum Zeitpunkt der Prüfung nicht erreichbar oder gar unbekannt sein können.<br />3. Bedarf es den Austausch der Personen-Identifikation zwischen dem Communties: "kennst du email@domain.com?". Diese Daten können verschlüsselt werden z.B mit `hash(salt,email)` welche dann an jede Community geschickt werden: kennst du `hash(salt,email), salt`? Was dazu führt, dass jede Community den hash aller seiner EMails errechen muss - die Skalierung ist entsprechend schlecht.<br /><br />Alternativ kann hier eine Blockchain eingesetzt werden, welche `hash(salt,email), salt` speichert und als dezentrales Nachschlagewerk für alle zugänglich ist. Hier erwarte ich ein Konzept, bevor wir das umsetzen können. Die Sicherheit der Nutzerdaten ist ebenfalls genau zu untersuchen, wenn wir das ganze ins Internet blasen. |
|
||||
| Claus-Peter 25.11.2021 | Ja da gebe ich dir nach heutigem Stand vollkommen Recht, dass es dafür derzeit keine 100% technische Lösung gibt.<br />Trotzdem ist das Thema fachlich gewünscht und wir müssen uns Gedanken machen, wie wir dafür eine technische Lösung finden können, die nahezu an die Anforderungen heranreicht. Genau deshalb endet dieser Absatz mit dem Hinweis auf die technsiche Konzeption. |
|
||||
|
||||
*Hier soll beschrieben werden, was den Unterschied zu eigenständigen und sich gegenseitig vertrauenden Communities zu den hier abhängigen (sprich vererbten) Communities auszeichnet, welche Vorraussetzungen bedarf der Auf/Abbau einer abhängigen Community und welche Konsequenzen hat der Auf- und Abbau von abhängigen Communities.*
|
||||
|
||||
Das Modell der *abhängigen Communities* findet seinen Ursprung den Föderalismus von Deutschland in einer Community-Struktur abbilden zu können. Das bedeutet, dass eine baumartige Struktur von Communities aufgebaut werden kann, wie nachfolgendes Bild schemenhaft zeigt:
|
||||
### Hirarchische Community
|
||||
|
||||
Um die Vision Gradido als Währung nicht nur in Communities als gemeinsame Interessensgemeinschaften zu etablieren, sondern auch für ganze Communen, Bundesländer, Nationen oder gar weltweit, bedarf es einer Strukturierung von Communities. Dazu dient das Konzept der *hierarchischen Community*, seinen Ursprung in der Abbildung des Föderalismus von Deutschland findet. Das bedeutet, dass eine baumartige Struktur von Communities aufgebaut werden kann, wie nachfolgendes Bild schemenhaft zeigt:
|
||||
|
||||
| PR-Kommentar | |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 25.11.2021 | Ich denke Förderalismus wie in der Bundesrepublik ist mit Gradido nicht möglich. Es ergibt sich durch das Schwundgeld einfach keine Vorteile eines Förderalismus.<br />Szenario Straßenbau zwischen zwei Communties:<br />- Warum sollte Geld von beiden Communities auf ein drittes Konto fließen, wenn es dort doch nur vergeht?<br />- Ist es nicht realistischer, dass beide Communties sich auf den Straßenbau einigen und das Geld direkt an den Auftragnehmer überweisen, um dem Schwund so weit es geht zu entgehen.<br /><br />Warum sollte sich eine Community einer anderen unterordnen? Was sind die Vorteile?<br />*Nur ein Gedanke* |
|
||||
| Claus-Peter 25.11.2021 | Ich kann deinen Gedanken und Bedenken folgen, doch andererseits kann ich auch dem Föderalismus etwas abgewinnen.<br />Klar würde sich eine Community schwer tun, sich einer anderen Community unterzuordnen. Doch genau da beginnt die Überlegung, wie man ein Community-übergreifendes Projekt organisieren könnte? Da gibt es schon Vorteile, die natürlich noch feiner konzipiert und ausformuliert werden müssen. Daher sollten wir die Hierarchie von Communities nicht im Vorhinein ausschließen. |
|
||||
|
||||
|
||||

|
||||
|
||||
Es wird somit zwischen zwei Communities aus benachbarten Ebenen eine Parent-Child-Beziehung erzeugt. Dadurch gehen diese beiden Communities eine besondere Beziehung untereinander ein, die zu folgenden veränderten Eigenschaften und Verhalten der Parent- und der Child-Community führen:
|
||||
Es wird somit zwischen zwei Communities aus direkt benachbarten Ebenen eine Parent-Child-Beziehung erzeugt. Dadurch gehen diese beiden Communities eine besondere Beziehung untereinander ein, die zu folgenden veränderten Eigenschaften und Verhalten der Parent- und der Child-Community führen:
|
||||
|
||||
###### Parent-Community
|
||||
#### Parent-Community
|
||||
|
||||
* kann 1 bis n Child-Communities besitzen
|
||||
* verwaltet keine Mitglieder mit AGE-Konto
|
||||
@ -73,14 +126,14 @@ Es wird somit zwischen zwei Communities aus benachbarten Ebenen eine Parent-Chil
|
||||
* bedarf spezieller Administrationsprozesse zur Verwaltung der Parent-Aufgaben:
|
||||
* Auf- und Abbau der Parent-Child-Beziehung
|
||||
* Verschiebung aller Mitglieder von der Parent- in die Child-Community
|
||||
* Stoppen des Sicherstellungsprozesses, dass eine *natürliche Person* nur Mitglied einer einzigen Community ist, sobald die erste Child-Beziehung aufgebaut ist und alle Mitglieder dahin verschoben sind
|
||||
* Stoppen des Sicherstellungsprozesses, dass eine *natürliche Person* nur Mitglied einer einzigen Community ist, sobald die erste Child-Beziehung aufgebaut ist und alle Mitglieder dorthin verschoben sind
|
||||
* Prozess zur Aufnahme der geschöpften Allgemeinwohl- und AUF-Gelder aus den Child-Communities
|
||||
* stoppt den Schöpfungsprozess sobald eine Child-Beziehung aufgebaut ist
|
||||
* startet den Schöpfungsprozess sobald die letzte Child-Beziehung aufgelöst ist
|
||||
* Aufnahmeprozess von Mitgliedern aus einer Child-Community, bevor dessen Beziehung aufgelöst wird
|
||||
* starten des Sicherstellungsprozesses, dass eine *natürliche Person* nur Mitglied einer einzigen Community ist, sobald die letzte Child-Beziehung aufgelöst ist
|
||||
|
||||
###### Child-Community
|
||||
#### Child-Community
|
||||
|
||||
* besitzt genau eine Parent-Community
|
||||
* **sofern es eine Community der untersten Ebene ist:**
|
||||
@ -94,32 +147,38 @@ Es wird somit zwischen zwei Communities aus benachbarten Ebenen eine Parent-Chil
|
||||
* hier läuft der Prozess zur Sicherstellung, dass eine *natürliche Person* nur Mitglied einer einzigen (Child)-Community ist
|
||||
*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##### Mischung aus den vorherigen Modellen
|
||||
|
||||
*Hier soll beschrieben werden welche möglichen Mischungen von Modellen erlaubt sind und welche nicht, was hat eine Mischungsvariante an Konsequenzen, wie wird eine Mischungsvariante auf/abgebaut, welche Vorraussetzungen bedarf es für den Auf/Abbau einer Mischungsvariante.**
|
||||
|
||||
### Geldschöpfung
|
||||
|
||||
Eine Community stellt die Mechanismen für die Dreifache-Geldschöpfung bereit. Dazu müssen zuerst die Verteilungsschlüssel auf die drei Kontoarten definiert bzw. konfigurierbar sein. Diese Konfigurationswerte werden vom Community-Administrator gepflegt. Sie dienen als Grundlage für die Höhe der regelmäßig geschöpften Beträge auf die drei Empfängerkonto-Typen. Die regelmäßige Geldschöpfung läuft automatisiert im Hintergrund und muss den Regeln der Nartürlichen Ökonomie des Lebens folgen. Die Details der Dreifachen Geldschöpfung sind in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) beschrieben.
|
||||
Eine Community stellt die Mechanismen für die Dreifache-Geldschöpfung bereit. Dazu müssen zuerst die Verteilungsschlüssel auf die drei Kontoarten definiert bzw. konfigurierbar sein. Diese Konfigurationswerte werden vom Community-Administrator gepflegt. Sie dienen als Grundlage für die Höhe der regelmäßig geschöpften Beträge auf die drei Empfängerkonto-Typen. Die regelmäßige Geldschöpfung läuft teilweise automatisiert im Hintergrund und muss den Regeln der Nartürlichen Ökonomie des Lebens folgen. Die Details der Dreifachen Geldschöpfung sind in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) beschrieben.
|
||||
|
||||
### Konto-Verwaltung
|
||||
|
||||
Durch die Dreifach-Geldschöpfung verwaltet die Community auch die drei Arten von Konten: Benutzerkonto, Gemeinwohlkonto und Ausgleichs- und Umweltkonto(AUF).
|
||||
Für die Dreifach-Geldschöpfung verwaltet die Community drei Arten von Konten: das AktiveGrundeinkommen-Konto pro Mitglied, das Community-eigene Gemeinwohlkonto und das Community-eigene Ausgleichs- und Umweltkonto(AUF).
|
||||
|
||||
Für jedes Mitglied der Community wird also ein eigenes Benutzerkonto verwaltet, auf das ein Drittel der monatlichen Geldschöpfung fließt. Das Gemeinwohlkonto und das AUF-Konto existieren pro Community einmal und auf jedes der beiden Konten fließen monatlich die beiden anderen Drittel der Geldschöpfung.
|
||||
Für jedes Mitglied der Community wird also ein eigenes AktiveGrundeinkommen-Konto verwaltet, auf das ein Drittel der monatlichen Geldschöpfung unter Einhaltung der AGE-Regeln fließt. Das Gemeinwohlkonto und das AUF-Konto existieren pro Community einmal und auf jedes der beiden Konten fließen monatlich die beiden anderen Drittel der Geldschöpfung.
|
||||
|
||||
Somit muss also eine Community für jede Kontoart die entsprechenden Kontoverwaltungsprozesse anbieten. Einmal in Verbindung pro Mitglied für das Benutzerkonto und dann jeweils eine Verwaltung für das Gemeinwohlkonto und eine Verwaltung für das AUF-Konto. Die Berechtigungen für die Zugriffe auf die drei Kontoarten müssen ebenfalls in der Community gepflegt und kontrolliert werden. Das bedeutet die Community muss ihren Mitgliedern auf ihre eigenen Benutzerkonten Zugriffsrechte erteilen und diese auch kontrollieren, so dass keine unerlaubten Zugriffe stattfinden können. Dann müssen in der Community bestimmte Mitglieder Sonderberechtigungen erhalten, um die Verwaltung des Gemeinwohlkontos und des AUF-Kontos durchführen zu können. Die Verwaltung der Berechtigungen ist wiederum alleine dem Community-Administrator erlaubt. Die Details der Kontenverwaltung ist im Dokument [KontenVerwaltung](.\KontenVerwaltung.md) beschrieben.
|
||||
Somit muss also eine Community für jede Kontoart die entsprechenden Kontoverwaltungsprozesse anbieten. Einmal in Verbindung pro Mitglied für das AGE-Konto und dann jeweils eine Verwaltung für das Gemeinwohlkonto und eine Verwaltung für das AUF-Konto. Die Berechtigungen für die Zugriffe auf die drei Kontoarten müssen ebenfalls in der Community gepflegt und kontrolliert werden. Das bedeutet die Community muss ihren Mitgliedern auf ihre eigenen AGE-Konten Zugriffsrechte erteilen und diese auch kontrollieren, so dass keine unerlaubten Zugriffe stattfinden können. Dann müssen in der Community bestimmte Mitglieder Sonderberechtigungen erhalten, um die Verwaltung des Gemeinwohlkontos und des AUF-Kontos durchführen zu können. Die Verwaltung der Berechtigungen ist wiederum alleine dem Community-Administrator erlaubt. Die Details der Kontenverwaltung ist im Dokument [KontenVerwaltung](.\KontenVerwaltung.md) beschrieben.
|
||||
|
||||
### Tätigkeitsverwaltung
|
||||
|
||||
Hier handelt es sich um eine Verwaltung von Tätigkeitsbeschreibungen, die von den Community-Mitgliedern als akzeptierte und berechtigte Leistungen zur Geldschöpfung als *Aktives Grundeinkommen* angesehen werden. Das heißt die Community muss unter den Mitgliedern eine Liste erarbeiten, die alle Tätigkeiten enthält, aus denen sich ein Mitglied dann eine oder mehrere auswählen kann, um sich sein Aktives Grundeinkommen damit zu decken. Die einzelnen Tätigkeiten sollen auch fachlich strukturierbar sein z.B. Kunst, Soziales, Gesundheit, Produktion, etc. . Die Menge und Definition der einzelnen Tätigkeiten und Strukturen unterliegt einer stetigen Anpassung nach den Bedürfnissen der Community-Mitglieder, um den natürlichen Veränderungen des miteinander Lebens gerecht werden zu können. Ob zu einer Tätigkeitsbeschreibung auch gleich eine Wertigkeit definiert werden soll, ist noch offen. Man kann aber sicherlich sagen, dass manche Tätigkeiten dem Gemeinwohl dienlicher sind als andere. Aber auch das ist wiederum eine Ansichtsache und muss unter den Community-Mitgliedern vereinbart werden.
|
||||
|
||||
| PR-Kommentar | |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Was ist wenn Tätigkeit A in Community X vergütet wird und Tätigkeit B in Community Y:<br />- Ich übe Tätigkeiten A & B aus<br />- Muss ich mich entscheiden, welche Tätigkeit ich vergütet wissen will? (Stichwort eindeutige Registrierung im gesammten Netzwerk) |
|
||||
| Claus-Peter 25.11.2021 | Gute Frage!<br />Ich denke wir sollten im 1. Schritt damit beginnen, dass jede Community ihre eigene Aktivitätenliste pflegt. Es werden dann wohl gleiche Aktivitäten in mehreren Listen der verschiedenen Communities auftauchen.<br /><br />Ich kann aber als Mitglied der Community A eine Tätigkeit A.X von einem Mitglied aus Community B bestätigt bekommen und natürlich auch von Mitgliedern aus meine Community A.<br /><br />Dazu müssen dann natürlich die notwendigen Informationen zw. den Communities für eine Cross-Community-Bestätigung ausgetauscht werden. Das führt dann genau zu dem gewünschten Effekt, dass zw. zwei Communities ein Informationsaustausch stattfindet, der dann eine Aussage über den Cross-Handel und das Vertrauen ermöglicht. |
|
||||
|
||||
|
||||
Zu der Liste der Tätigkeiten gibt es einen weiteren Prozess, der in dem Dokument [RegelnDerGeldschoepfung](./RegelnDerGeldschoepfung.md) näher beschrieben ist. Hier kann soviel erst einmal gesagt werden, dass die Tätigkeitenliste als Grundlage dient, damit ein Mitglied für seine erbrachten Leistungen für das Allgemeinwohl dann sein monatliches *Aktives Grundeinkommen* gutgeschrieben bekommt. Dieses Gutschreiben des AGEs unterliegt noch einer vorherigen Bestätigung von anderen Community- oder auch Community übergreifenden Mitgliedern. Somit erfolgt dadurch eine implizite Vernetzung der Mitglieder durch dieses aktive Bestätigen anderer Leistungen, was gleichzeitig wieder Vorraussetzung ist, um sein eigenes AGE zu erhalten.
|
||||
|
||||
### Berechtigungsverwaltung
|
||||
|
||||
Die Community muss für die verschiedenen Eigenschaften und Prozesse eine eigene Berechtigungsverwaltung zur Verfügung stellen. Für die verschiedenen Berechtigungen muss ein Rollen- und Rechte-Konzept administrierbar sein, so dass für die verschiedenen Mitglieder der Community die Zugriffe feingranular definiert, gesteuert und kontrolliert werden können. Allein der Administrator hat die Rechte auf die Berechtigungsverwaltung zuzugreifen. Das System muss diese hinterlegten Rollen und Rechte dann auf die verwalteten Mitglieder abbilden und für jeden Zugriff auf die Community entsprechend kontrollieren, freigeben oder verhindern.
|
||||
|
||||
### Vernetzung und Vertrauensbildung
|
||||
|
||||
Mit der Vernetzung der Communities und dem gemeinsamen Handel zwischen Community-Mitgliedern innerhalb des gesamten Netzwerks entsteht automatisch ein Vertrauensverhältnis zwischen den verschiedenen Communities und auch zwischen den Community-Mitgliedern. Diese sich dynamisch verändernde Vertrauensverhältnisse können als Graph aufbereitet und zu weiteren Auswertungen bzw. Priorisierungen von fachlichen Prozessen herangezogen werden. Da in dem Gradido-Netzwerk der Mensch und das gegenseitige Vertrauen im Mittelpunkt steht, benötigt er für seine Bewertungen und Entscheidungen von Handel und Austausch mit anderen Communities bzw. anderen Mitgliedern ein Werkzeug, das ihm diese Informationen liefern kann. Das bedeutet in der Gradido-Anwendung werden statistische Werte über die Kommunikation zwischen Communities und zwischen Mitgliedern erhoben, die als Grundlage für den Vertrauensgraphen dienen.
|
||||
|
||||
### Attribute einer Community
|
||||
|
||||
In diesem Kapitel werden die Attribute beschrieben, die in einer Community zu speichern sind.
|
||||
@ -166,27 +225,39 @@ Das Attribut *Parent* dient für den hierarchischen Aufbau von Communities. Es e
|
||||
|
||||
Das Attribut *Liste Children* dient ebenfalls dem hierarchischen Aufbau von Communities. Es enthält die Bezüge auf die Communities, die für diese Community als Child-Community eingerichtet sind. Eine Parent-Community kann mehrere Child-Communities haben. Durch diesen Bezug zu den Child-Communities werden einzelne Prozesse zwischen der Parent- und den Child-Communities freigeschalten. Damit ergeben sich erweiterte Möglichkeiten u.a. für die Community-Mitglieder beider Communities, wie beispielsweise das Community übergreifende Handeln zwischen den Community-Mitgliedern oder eine veränderte Verteiltung der Gemeinwohl- und AUF-Schöpfung, etc.. Die Administration dieses Attributes erfolgt implizit über die fachlichen Prozesse, die den Auf- und Abbau einer Parent-Child-Beziehung zwischen zwei Communities steuern. Diese können nur durch den Administrator und seiner Berechtigung ausgelöst werden. Die Beschreibung dieser Prozesse ist weiter unten im Kapitel **Anwendungsfälle auf einer Community** zu finden.
|
||||
|
||||
#### Liste Trusted Communities
|
||||
|
||||
Das Attribut *Liste Trusted Communities* dient dem Aufbau von gleichberechtigten Community-Gruppierungen. Es enthält die Referenzen auf die Communities, die für diese Community als vertrauenswürdige Communities eingerichtet sind. Eine vertrauenswürdige Community-Gruppierung kann mehrere gleichberechtigte Communities haben. Durch diesen Bezug zu den vertrauenswürdigen Communities werden einzelne Prozesse zwischen den sich gegenseitig vertrauenden Communities freigeschalten. Damit ergeben sich erweiterte Möglichkeiten u.a. für die Community-Mitglieder beider Communities, wie beispielsweise das Community übergreifende Handeln zwischen den Community-Mitgliedern, etc.. Zwischen zwei *Trusted Communities* erfolgt keine Verteilung gemäß einem Verteilungsschlüssel von geschöpftem Geld das für das Allgemeinwohl- bzw. AUF-Konto bestimmt ist. Dies bleibt Eigentum jeder Community trotz vertrauenswürdiger Beziehung untereinander.
|
||||
|
||||
Die Administration dieses Attributes erfolgt implizit über die fachlichen Prozesse, die den Auf- und Abbau einer vertrauenswürdigen Beziehung zwischen zwei Communities steuern. Diese können nur durch den Adminitrator und seiner Berechtigung ausgelöst werden. Die Beschreibung dieser Prozesse ist im nachfolgenden Kapitel **Anwendungsfälle auf einer Community** zu finden.
|
||||
|
||||
|
||||
## Anwendungsfälle auf einer Community
|
||||
|
||||
Die nachfolgenden Anwendungsfälle beschreiben die fachlichen Vorraussetzungen, den fachlichen Ablauf und die fachlichen Veränderungen bzw. den fachlichen Status, der am Ende des erfolgreich abgeschlossenen Anwendungsfalles erreicht wird. Desweiteren erfolgt die fachliche Beschreibung der möglichen Fehlerfälle, in die ein Anwendungsfall münden kann und welcher fachlicher Status am Ende des Anwendungsfalles herrschen soll.
|
||||
|
||||
### Neue Community erstellen
|
||||
|
||||
*Allgemeine fachliche Beschreibung des Anwendungsfalles.*
|
||||
Der Prozess *Neue Community erstellen* kann in zwei grundlegende Schritte untergliedert werden. Im ersten Schritt erfolgt der Aufbau und die Einrichtung der technischen Infrastruktur, die für den Betrieb der neuen Community benötigt wird. Im zweiten Schritt wird dann die eigentliche Inbetriebnahme der neuen Community durchgeführt, bei der die notwendigen Registrierungsschritte für den fachlichen Austausch an Informationen zwischen den schon bestehenden Communities und der neuen Community erfolgt. Der erste Schritt wird hier im Kapitel Vorraussetzungen beschrieben. Der zweite Schritt dieses Prozesse erfolgt als Ablaufbeschreibung der Registrierungsschritte im Kapitel Ablauf.
|
||||
|
||||
#### Vorraussetzungen
|
||||
|
||||
Um eine neue Community zu erstellen wird eine dafür speziell konzepierte Infrastruktur benötigt. Die technischen Details dieser Infrastruktur werden in der *technischen Infrastruktur Beschreibung* als eigenständiges Dokument dem Administrator der neuen Community zur Verfügung gestellt. Diese ist neben den Installationsskripten und Anwendungsdateien Teil des Auslieferungspaketes der Gradido-Anwendung.
|
||||
|
||||
Sobald der Administrator die geforderte Infrastruktur in Betrieb genommen und darauf die entsprechenden Installationsskripte ausgeführt hat erfolgt die eigentliche Erstellung und Registrierung der neue Community. Das heißt beim erstmaligen Start der Gradido-Anwendung wird automatisch der Prozess *Neue Community erstellen* gestartet.
|
||||
|
||||
#### Ablauf
|
||||
|
||||
Der Prozess *Neue Community erstellen* wird entweder automatisiert beim erstmaligen Start der Gradido-Anwendung auf einer neuen Infrastruktur gestartet oder manuell, wenn eine neue Community auf einer schon bestehenden Infrastruktur zusätzlich eingerichtet werden soll. Die nachfolgende Ablaufgrafik zeigt die logischen Schritte, die in dem Prozess durchlaufen werden:
|
||||
|
||||

|
||||
|
||||
#### Ende Status
|
||||
|
||||
1. Community-Infrastruktur ist installiert und aktiv
|
||||
2. neue Community ist erzeugt und Daten in der Community-DB gespeichert
|
||||
3. der Hintergrundprozess "Community-Vernetzung" ist am Laufen
|
||||
* die initiale "newCommunity-Msg" mit den eigenen Community-Daten ist in den Public-Channel versendet
|
||||
* ein Listener lauscht am Public-Channel auf Antworten (replyNewCommunityMsg) der schon existenten Communities
|
||||
* ein Listener lauscht am Public-CHannel auf initiale "newCommunity-Msg" anderer neuer Communities
|
||||
4. mit dem ersten Empfangen einer Reply-Msg einer anderen Community, wird der Community-Connection Prozess gestartet, der mit jedem Empfang von neuen Community-Daten eine P2P-Verbindung zu dieser Community aufbaut, um direkt detaillierte Daten auszutauschen
|
||||
5. die vordefinierte Tätigkeitsliste ist geladen
|
||||
6. die vordefinierten Berechtigungen sind aktiv
|
||||
7. optional sind schon Mitglieder erfasst und in der Datenbank gespeichert
|
||||
|
||||
#### Fehlerfälle
|
||||
|
||||
### Community bearbeiten
|
||||
@ -260,3 +331,78 @@ Die nachfolgenden Anwendungsfälle beschreiben die fachlichen Vorraussetzungen,
|
||||
#### Ende Status
|
||||
|
||||
#### Fehlerfälle
|
||||
|
||||
# Besprechung 19.08.2021 19:00 mit Bernd
|
||||
|
||||
## Kreis-Mensch-Sein-Community
|
||||
|
||||
Felix Kramer
|
||||
|
||||
noch keine eigene Währung, wollen gerne Gradido
|
||||
|
||||
haben auch aktives Grundeinkommen
|
||||
|
||||
passt aber nicht ganz zur Gradido Philosophie, weil Gemeinwohlleistung zu unterschiedlich bewertet werden.
|
||||
|
||||
-> Colored Gradido?
|
||||
|
||||
Community-Creation
|
||||
|
||||
GDD1 (gold) ist existent
|
||||
|
||||
Felix baut GGD2-Infrastruktur auf
|
||||
|
||||
* Frage: willst du GDD1(gold) oder eigene Währung?
|
||||
* Antwort: nein ich will eigene GDD2 (rot)
|
||||
* muss neue Währung erzeugen
|
||||
* Antwort: ja, dann Anfrage an GDD1, dass GDD2 auch Goldene GDD1 schöpfen darf?
|
||||
* Ja wird akzeptiert
|
||||
* dann bekommt GDD2 die Lizenz goldene GDD1 schöpfen kann
|
||||
|
||||
Kommt später heraus, dass GDD2 nicht mehr den goldenen Regeln entspricht, dann muss die Lizenz zum goldene GDD1 Schöpfen für GDD2 gesperrt werden.
|
||||
|
||||
Bisher geschöpfte goldene GDD2 beleiben aber erhalten.
|
||||
|
||||
Es darf keine Markierung des Bot-Mitglieds geben, da Missbrauch/Fehler möglich
|
||||
|
||||
Identität für ein Mitglied muss Human/Nichthuman enthalten
|
||||
|
||||
GDD2 muss mit Lizenzentzug wechseln auf eigene Währung um weiterschöpfen zu können.
|
||||
|
||||
Mitgliederwechsel in andere Community muss dann auch Währungswechsel berücksichtigen.
|
||||
|
||||
Bestcase: 1 Blockchain pro Währung
|
||||
|
||||
GDD1(gold) existent
|
||||
|
||||
GDD2(gold) soll gegründet werden
|
||||
|
||||
GDD2 baut Infrasturktur auf
|
||||
|
||||
Frage an GDD2, ob goldene oder andere?
|
||||
|
||||
### Tätigkeiten, die von der Community aktzeptiert werden
|
||||
|
||||
Nachweise für durchgeführte Tätigkeiten, bevor diese dem AGE-Konto gutgeschrieben werden?
|
||||
|
||||
Liste der Tätigkeiten muss von Community erstellt, bestätigt und verwaltet werden
|
||||
|
||||
Bei Tätigkeit von x Stunden für das AGE muss aus der Liste die passende Tätigkeit gewählt werden und per Nachweis (andere Mitglieder, Video, o.ä.)
|
||||
|
||||
Bei Krankheit o.ä. muss es aber möglich sein, dass dennoch Geld auf das AGE-Konto kommt.
|
||||
|
||||
| PR-Kommentar | |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Definition? Ist Faulheit eine Krankheit? |
|
||||
| Claus-Peter 25.11.2021 | :-) Das kommt auf die solidarische Einstellung der Community an ;-)<br /><br />da drängt sich mir die Gegenfrage auf: Bis zu welchem Alter bekommt ein Kind sein AGE-Geld geschöpft nur durch seine blos Existenz? Oder andersherum, ab und bis zu welchem Alter muss eine Gegenleistung erbracht werden, Stichworte: unbeschwerte Kindheit und wohlverdiente Altersruhe? |
|
||||
|
||||
|
||||
Kontaktförderung durch gewichtete Tätigkeitsbestätigung ( bei mind. 2 Bestätigungen pro Tätigkeit muss mind. ein neues Mitglied dabei sein)
|
||||
|
||||
Liste von Mitgliedern, die ich bestätigt habe:
|
||||
|
||||
* Kontaktpflege
|
||||
* Gewichtung
|
||||
* Vernetzung
|
||||
|
||||
Ricardo Leppe Podcast Lern und Memotechniken
|
||||
|
||||
@ -1,3 +1,33 @@
|
||||
# Regeln der Geldschöpfung
|
||||
|
||||
*Hier werden die fachlichen Regeln der 3-fachen Schöpfung beschrieben*
|
||||
Nach der Gradido-Philosophie erfolgt pro Monat der Prozess der Dreifachen-Geldschöpfung. Das bedeutet, dass in einer Gradido-Community pro Mitglied, das eine *natürliche Person* ist, eine maximale Summe von 3.000 GDD geschöpft wird. Davon kann das Mitglied 1.000 GDD als *Aktives Grundeinkommen* durch Community nützige Leistungen auf sein persönliches AGE-Konto erhalten. 1.000 GDD werden auf das Gemeinwohlkonto der Community gutgeschrieben und weitere 1.000 GDD fließen auf das AUF-Konto der Community.
|
||||
|
||||
| PR-Kommentar | |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Hier wird immer ausgespart wie sich die 3x 1000 GDD zueinander verhalten. Als ich Bernd gefragt habe sagte er, das aus Planungssicherheit immer die vollen 2000 GDD für die Community geschöpft werden. Das halte ich allerdings für sehr gefährlich, da es dazu verleitet einfach weitere Konten zu registrieren. Hinter dem geschöpftem Geld steht dann auch ggf keine Leistung und damit keine Deckung. Ich halte es für essentiell die Schöpfung für AUF & Community GDD auch an die konkrete Schöpfung des Nutzers zu binden, da diese leistungsgedeckt ist. |
|
||||
| Claus-Peter 25.11.2021 | Das ist eine berechtigte Frage und muss echt konzeptionell weiter durchdacht werden.<br />Ein Hinweis zu dem Thema wäre noch, dass egal ob und wieviel eine Person in dem Giralgeldsystem verdient, die Kommune, in der diese Person den 1.Wohnsitz hat, auch Steuergelder zugewiesen bekommt. |
|
||||
|
||||
|
||||
Um diese Schritte des Geldschöpfungsprozesses zu gewährleisten bedarf es einiger Vorraussetzungen, die im folgenden beschrieben werden.
|
||||
|
||||
# Vorraussetzungen
|
||||
|
||||
## Drei Kontenmodell
|
||||
|
||||
## Verteilungsschlüssel
|
||||
|
||||
## Tätigkeitsliste für Aktives Grundeinkommen
|
||||
|
||||
Die Tätigkeitsliste für das AGE dient als Grundlage der gemeinnützigen Leistungen, die ein Mitglied sich anrechnen lassen kann. Das heißt eine Community muss eine Sammlung von Tätigkeiten erfassen, die von den Community-Mitgliedern als gemeinnützig empfunden und bestätigt werden.
|
||||
|
||||
Das heißt die Community muss unter den Mitgliedern eine Liste erarbeiten, die alle Tätigkeiten enthält, aus denen sich ein Mitglied dann eine oder mehrere auswählen kann, um sich sein Aktives Grundeinkommen damit zu decken. Die einzelnen Tätigkeiten sollen auch fachlich strukturierbar sein z.B. Kunst, Soziales, Gesundheit, Produktion, etc. . Die Menge und Definition der einzelnen Tätigkeiten und Strukturen unterliegt einer stetigen Anpassung nach den Bedürfnissen der Community-Mitglieder, um den natürlichen Veränderungen des miteinander Lebens gerecht werden zu können. Ob zu einer Tätigkeitsbeschreibung auch gleich eine Wertigkeit definiert werden soll, ist noch unklar. Man kann aber sicherlich sagen, dass manche Tätigkeiten dem Gemeinwohl dienlicher sind als andere. Aber auch das ist wiederum eine Ansichtsache und muss unter den Community-Mitgliedern vereinbart werden.
|
||||
|
||||
| PR-Kommentar | |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Ulf 07.11.2021 | Ich denke die Liste sollte sich aus den Tätigkeiten der anerkannten Leistungen zusammensetzen, folglich vollautomatisch entstehen.<br />Die Zuordnung jeder Leistung zu einer Kategorie halte ich allerdings für sehr gut, da es einen Vergleich und eine Bewertung von Communities zulässt "Das ist eine Handwerker Community" "Das sind ja nur Baumschmuser" |
|
||||
| Claus-Peter 25.11.2021 | Ich denke im ersten Schritt können wir aus den bisher aufgelaufenen Tätigkeiten während der Transformationsphase eine Art Default-Liste erzeugen, die dann von den Communities weiter ergänzt/verändert, etc. werden.<br />Ob wir hier evtl. schon Schrittweise in den jetzigen Adminbereich Mechanismen einbauen sollten, um eine Gliederung und feste Vorschlagstexte der Tätigkeitsbezeichnungen zu gewinnen, sollten wir im Hinterkopf behalten. |
|
||||
|
||||
|
||||
### Erstellung und Erfassung der Tätigkeiten
|
||||
|
||||
# Prozess der Geldschöpfung
|
||||
|
||||
@ -0,0 +1,304 @@
|
||||
<mxfile>
|
||||
<diagram id="Lc_Wy6ZhKx3Be9Prl_QG" name="Page-1">
|
||||
<mxGraphModel dx="1088" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="28" value="Community-Prozess" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#82b366;strokeWidth=2;fillColor=#d5e8d4;gradientColor=#97d077;fontColor=#000000;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="130" width="1220" height="910" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="Prozess: Community-Vernetzung" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#6c8ebf;strokeWidth=2;fillColor=#dae8fc;gradientColor=#7ea6e0;fontColor=#000000;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="610" y="264.5" width="640" height="555.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontColor=#000000;strokeColor=#000000;" parent="1" source="2" target="4" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="185" y="170"/>
|
||||
<mxPoint x="250" y="170"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="automatisch beim 1.Start der Anwendung" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="170" y="40" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="3" target="4" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="250" y="210" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="405" y="170"/>
|
||||
<mxPoint x="250" y="170"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="77" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="3" target="75">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="405" y="170"/>
|
||||
<mxPoint x="450" y="170"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="95" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.75;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="3" target="38">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="560" y="70"/>
|
||||
<mxPoint x="560" y="790"/>
|
||||
<mxPoint x="485" y="790"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="manuell durch Administrator" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="40" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;" parent="1" source="4" target="5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Initialisiere Prozess <br>"Neue Community erstellen"" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="202.63" width="200" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;" parent="1" source="5" target="7" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Attribute der Community erfassen <br>bzw. aus Config lesen<br><div style="text-align: left"><span>- Name</span></div><div style="text-align: left"><span>- Icon / Bild (opt.)</span></div><div style="text-align: left"><span>- Beschreibung</span></div><div style="text-align: left"><span>- hosted Server / URL</span></div><div style="text-align: left"><span>- Währungsname (opt.)</span></div><div style="text-align: left"><span>- Währungskürzel (opt.)</span></div>" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="280" width="200" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#000000;exitX=0.75;exitY=0;exitDx=0;exitDy=0;" parent="1" source="24" target="13" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="520" y="420" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="425" y="451"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="51" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#1A1A1A;" edge="1" parent="1" source="7" target="50">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="erzeuge techn. Community-Keys:<br><div style="text-align: left"><span>- Community-ID</span></div><div style="text-align: left"><span>- Community-Currency-ID</span></div>" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="423.13" width="200" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;" parent="1" source="13" target="16" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="70" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="13" target="72">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="785" y="364.5"/>
|
||||
<mxPoint x="785" y="364.5"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="74" value="Nein" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontColor=#000000;labelBackgroundColor=#B0E3E6;rounded=1;" vertex="1" connectable="0" parent="70">
|
||||
<mxGeometry x="-0.2906" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="Community-Attribute: (Name, Community-ID, URL) vorhanden?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="690" y="410.5" width="190" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="1" source="16" target="20" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="785" y="517.13" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="Sende Community-Attribute<br>auf public Channel" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="960" y="429.76" width="210" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="83" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="20" target="80">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="lausche an public Channel<br>auf<br>"replyNewCommuntiy-Msg"<br>"newCommuntiy-Msg"" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="960" y="511.62999999999994" width="210" height="65" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#000000;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="22" target="20" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="910" y="544"/>
|
||||
<mxPoint x="910" y="544"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="speichere empfangene<br>Community-Daten in<br>Community-DB" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="690" y="516.6299999999999" width="210" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="Starte Community-Vernetzung <br>als Hintergrundprozess" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="290" y="622.63" width="180" height="39.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="37" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;" parent="1" source="34" target="36" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="Lade <br>Default-Tätigkeitsliste,<br>Standard-Berechtigungen,<br>etc." style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="130" y="607.38" width="140" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="46" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;" parent="1" source="36" target="39" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="47" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#000000;rounded=1;labelBackgroundColor=#97D077;" parent="46" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.24" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="48" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;fontColor=#000000;strokeColor=#000000;" parent="1" source="36" target="43" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="49" value="Nein" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontColor=#000000;rounded=1;labelBackgroundColor=#97D077;" parent="48" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.2056" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="neue Mitglieder<br>erfassen?" style="rhombus;whiteSpace=wrap;html=1;fontColor=#ffffff;strokeColor=#2D7600;strokeWidth=2;fillColor=#60a917;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="130" y="754.5" width="140" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="Community-Prozess" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#82b366;strokeWidth=2;fillColor=#d5e8d4;gradientColor=#97d077;fontColor=#000000;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="290" y="834.5" width="260" height="120" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="45" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontColor=#FFFFFF;strokeColor=#000000;" parent="1" source="39" target="43" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="510" y="990"/>
|
||||
<mxPoint x="200" y="990"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="Prozess <br>"Neuen Benutzer anlegen"" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="330" y="874.5" width="200" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" value="" style="ellipse;html=1;shape=endState;fillColor=#000000;strokeColor=#000000;labelBackgroundColor=#97D077;fontColor=#FFFFFF;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="185" y="1060" width="30" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="53" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0.514;entryDx=0;entryDy=0;entryPerimeter=0;strokeColor=#1A1A1A;" edge="1" parent="1" source="50" target="52">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="50" value="speichere Community-Daten <br>in Community-DB" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="150" y="503.13" width="200" height="39.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="54" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;strokeColor=#1A1A1A;exitX=0.374;exitY=0.232;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" parent="1" source="52" target="24">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="302" y="596"/>
|
||||
<mxPoint x="380" y="596"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="55" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;strokeColor=#1A1A1A;exitX=-0.226;exitY=0.784;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="52" target="34">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="52" value="" style="html=1;points=[];perimeter=orthogonalPerimeter;fillColor=#000000;strokeColor=none;rotation=90;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="250" y="480.13" width="5" height="185" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="62" value="newCommunity-Msg" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=0;notch2=20;align=center;verticalAlign=middle;fontSize=14;fontStyle=0;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;rotation=15;" vertex="1" parent="1">
|
||||
<mxGeometry x="1184.99" y="463.13" width="170" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="69" value="<font style="font-size: 12px">replyNewCommunity-Msg</font>" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=20;notch2=0;align=left;verticalAlign=middle;fontSize=14;fontStyle=0;flipH=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="1180.01" y="519.51" width="160" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="73" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="72" target="5">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="72" value="Fehlermeldung <br>wegen<br>fehlender Parameter" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;strokeColor=#2D7600;fillColor=#B0E3E6;fontColor=#000000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="725" y="314.5" width="120" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="76" style="edgeStyle=orthogonalEdgeStyle;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="75" target="13">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="450" y="451"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="75" value="Starte Community-Vernetzung <br>als Hintergrundprozess" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="360" y="202.63" width="180" height="39.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="79" value="<font style="font-size: 12px">newCommunity-Msg</font>" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=20;notch2=0;align=left;verticalAlign=middle;fontSize=14;fontStyle=0;flipH=1;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;rotation=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="1180.01" y="546.39" width="160" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="81" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="80" target="22">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="82" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontColor=#000000;labelBackgroundColor=#7EA6E0;" vertex="1" connectable="0" parent="81">
|
||||
<mxGeometry x="-0.4267" y="2" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="85" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="80" target="84">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="80" value="replyNewCommunityMsg?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="970" y="588.76" width="190" height="47.87" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="93" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0.499;exitY=0.966;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="84" target="86">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="1065" y="717.8699999999999" as="sourcePoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="96" value="Ja" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=14;fontColor=#000000;labelBackgroundColor=#7EA6E0;" vertex="1" connectable="0" parent="93">
|
||||
<mxGeometry x="-0.2741" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="84" value="NewCommunityMsg?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="970" y="650" width="190" height="47.87" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="94" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="86" target="22">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="86" value="Sende Community-Attribute<br>auf public Channel" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="960" y="730" width="210" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="89" value="replyNewCommunity-Msg" style="html=1;shape=mxgraph.infographic.ribbonSimple;notch1=0;notch2=20;align=left;verticalAlign=middle;fontSize=14;fontStyle=0;fillColor=#d5e8d4;strokeColor=#82b366;fontColor=#000000;rounded=1;rotation=-45;" vertex="1" parent="1">
|
||||
<mxGeometry x="1179.98" y="657.38" width="180.01" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="97" value="" style="group" vertex="1" connectable="0" parent="1">
|
||||
<mxGeometry x="1359.9950000000001" y="460.0049999999999" width="232.6199999999999" height="182.6300000000001" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="65" value="" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;align=center;rotation=-90;strokeColor=#36393d;fillColor=#66B2FF;rounded=1;" vertex="1" parent="97">
|
||||
<mxGeometry x="24.99499999999989" y="-24.99499999999989" width="182.63" height="232.62" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="66" value="public Channel" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;fontColor=#000000;fontSize=14;rounded=1;" vertex="1" parent="97">
|
||||
<mxGeometry x="96.30500000000006" y="74.99500000000012" width="40" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="98" value="Prozess: Community-Connection" style="html=1;align=left;verticalAlign=top;absoluteArcSize=1;arcSize=18;dashed=0;spacingTop=10;spacingRight=30;strokeColor=#6c8ebf;strokeWidth=2;fillColor=#dae8fc;gradientColor=#7ea6e0;fontColor=#000000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="610" y="834.5" width="640" height="185.5" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="106" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;fillColor=#ffffff;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="99" target="105">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="99" value="stelle P2P-Verbindung zu <br>neuer Community her<br>und tausche detailiete Daten aus" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="974.99" y="881" width="210" height="67" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="102" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;" edge="1" parent="1" source="100" target="99">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="100" value="Daten einer neuen Community erhalten?" style="rhombus;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;align=center;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="637" y="874.5" width="190" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="101" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="22" target="100">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="670" y="544"/>
|
||||
<mxPoint x="670" y="790"/>
|
||||
<mxPoint x="732" y="790"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="103" value="" style="shape=flexArrow;endArrow=classic;startArrow=classic;html=1;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;fillColor=#ffffff;" edge="1" parent="1">
|
||||
<mxGeometry width="100" height="100" relative="1" as="geometry">
|
||||
<mxPoint x="1190.01" y="914" as="sourcePoint"/>
|
||||
<mxPoint x="1340.01" y="914" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="104" value="neue Community" style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fontSize=14;fontColor=#000000;fillColor=#B0E3E6;align=center;gradientColor=#97D077;" vertex="1" parent="1">
|
||||
<mxGeometry x="1359.99" y="840" width="240.01" height="160" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="107" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=14;fontColor=#000000;strokeColor=#1A1A1A;fillColor=#ffffff;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="105" target="100">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="105" value="speichere empfangene<br>Community-Daten in<br>Community-DB" style="html=1;align=center;verticalAlign=top;absoluteArcSize=1;arcSize=10;dashed=0;fillColor=#b0e3e6;strokeColor=#0e8088;fontColor=#000000;rounded=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="810" y="954.5" width="150" height="55" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 50 KiB |
BIN
docu/Concepts/BusinessRequirements/image/CommunityNetwork.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 25 KiB |
BIN
docu/Concepts/BusinessRequirements/image/RegistrationProcess.png
Normal file
|
After Width: | Height: | Size: 329 KiB |
100
docu/graphics/roadmap.drawio
Normal file
@ -0,0 +1,100 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="5Rbgv0eSL3tDJ_YiGKbA" name="Page-1">
|
||||
<mxGraphModel dx="1406" dy="633" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="Release 1.6.X" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="40" width="560" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Development<br style="font-size: 12px">15.10.2021 -<br style="font-size: 12px">30.12.2021&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="120" width="400" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Testing<br style="font-size: 7px;">31.12.2021 -<br style="font-size: 7px;">07.01.2022&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
|
||||
<mxGeometry x="440" y="120" width="40" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Release Window<br style="font-size: 7px;">08.01.2022 -<br style="font-size: 7px;">15.01.2022&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="120" width="40" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Deadline<br style="font-size: 12px;">31.01.2022&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="520" y="120" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Release 1.7.X" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="40" width="560" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Admin Interface" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="200" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="Replace Login Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="240" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="Replace Community Server" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="280" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" value="Refine/Partially redesign Register Process" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="320" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="PublisherID Field on Register" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="360" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" value="GDD Decay Calculator Tool" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="400" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" value="Refactors, Tests &amp; Devops (e.g. Seeding)" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="560" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Seperate Overview from SendCoin" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="440" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="Minor Design &amp; Layout changes in Frontend" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="480" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
|
||||
<mxGeometry x="440" y="200" width="40" height="400" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
|
||||
<mxGeometry x="480" y="200" width="40" height="400" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="New Deployment" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="520" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="Start refactoring" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="520" y="200" width="80" height="400" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="Development<br style="font-size: 12px">01.02.2022 -<br style="font-size: 12px">28.02.2022&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="120" width="400" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="Testing<br style="font-size: 12px">01.03.2022 -<br style="font-size: 12px">07.03.2022&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="1000" y="120" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="Release Window<br style="font-size: 12px">08.03.2022 -<br style="font-size: 12px">15.03.2022&nbsp;" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="1080" y="120" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="Refactoring" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="200" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
|
||||
<mxGeometry x="1000" y="200" width="80" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="" style="rounded=0;whiteSpace=wrap;html=1;fontSize=7;" parent="1" vertex="1">
|
||||
<mxGeometry x="1080" y="200" width="80" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="Admin Interface User Tools" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="240" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="Make Klicktipp Community Ready" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="280" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="Make GDT Community Ready" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="320" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="Implement a Federation Solution &amp; Experiment" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="360" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="Frontend Community/Account statistics" style="rounded=0;whiteSpace=wrap;html=1;fontSize=12;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="400" width="400" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
BIN
docu/graphics/roadmap.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
@ -47,8 +47,18 @@ export const logout = gql`
|
||||
`
|
||||
|
||||
export const transactionsQuery = gql`
|
||||
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
query(
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$onlyCreations: Boolean = false
|
||||
) {
|
||||
transactionList(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
onlyCreations: $onlyCreations
|
||||
) {
|
||||
gdtSum
|
||||
count
|
||||
balance
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
"at": "am",
|
||||
"cancel": "Abbrechen",
|
||||
"close": "schließen",
|
||||
"current_balance": "aktueller Kontostand",
|
||||
"date": "Datum",
|
||||
"description": "Beschreibung",
|
||||
"edit": "bearbeiten",
|
||||
@ -66,6 +67,7 @@
|
||||
"lastname": "Nachname",
|
||||
"memo": "Nachricht",
|
||||
"message": "Nachricht",
|
||||
"new_balance": "neuer Kontostand nach Bestätigung",
|
||||
"password": "Passwort",
|
||||
"passwordRepeat": "Passwort wiederholen",
|
||||
"password_new": "neues Passwort",
|
||||
@ -90,7 +92,8 @@
|
||||
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
|
||||
"usernmae-regex": "Der Username muss mit einem Buchstaben beginnen auf den mindestens zwei alfanumerische Zeichen folgen müssen.",
|
||||
"usernmae-unique": "Der Username ist bereits vergeben."
|
||||
}
|
||||
},
|
||||
"your_amount": "Dein Betrag"
|
||||
},
|
||||
"gdt": {
|
||||
"action": "Aktion",
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
"at": "at",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"current_balance": "current balance",
|
||||
"date": "Date",
|
||||
"description": "Description",
|
||||
"edit": "Edit",
|
||||
@ -66,6 +67,7 @@
|
||||
"lastname": "Lastname",
|
||||
"memo": "Message",
|
||||
"message": "Message",
|
||||
"new_balance": "account balance after confirmation",
|
||||
"password": "Password",
|
||||
"passwordRepeat": "Repeat password",
|
||||
"password_new": "New password",
|
||||
@ -90,7 +92,8 @@
|
||||
"is-not": "You cannot send Gradidos to yourself",
|
||||
"usernmae-regex": "The username must start with a letter, followed by at least two alphanumeric characters.",
|
||||
"usernmae-unique": "The username is already taken."
|
||||
}
|
||||
},
|
||||
"your_amount": "Your amount"
|
||||
},
|
||||
"gdt": {
|
||||
"action": "Action",
|
||||
|
||||
@ -251,6 +251,44 @@ describe('Login', () => {
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('error.no-account')
|
||||
})
|
||||
|
||||
describe('login fails with "User email not validated"', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: 'User email not validated',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('redirects to /thx/login', () => {
|
||||
expect(mockRouterPush).toBeCalledWith('/thx/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('login fails with "User has no password set yet"', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: 'User has no password set yet',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('redirects to /reset/login', () => {
|
||||
expect(mockRouterPush).toBeCalledWith('/reset/login')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -105,10 +105,10 @@ export default {
|
||||
loader.hide()
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.message.includes('No user with this credentials')) {
|
||||
this.$toasted.global.error(this.$t('error.no-account'))
|
||||
} else {
|
||||
// : this.$t('error.no-email-verify')
|
||||
this.$toasted.global.error(this.$t('error.no-account'))
|
||||
if (error.message.includes('User email not validated')) {
|
||||
this.$router.push('/thx/login')
|
||||
} else if (error.message.includes('User has no password set yet')) {
|
||||
this.$router.push('/reset/login')
|
||||
}
|
||||
loader.hide()
|
||||
|
||||
@ -13,7 +13,9 @@ describe('SendOverview', () => {
|
||||
|
||||
const propsData = {
|
||||
balance: 123.45,
|
||||
transactionCount: 1,
|
||||
GdtBalance: 1234.56,
|
||||
transactions: [{ balance: 0.1 }],
|
||||
pending: true,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
@ -42,18 +44,16 @@ describe('SendOverview', () => {
|
||||
expect(wrapper.find('div.gdd-send').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
// it('has a transactions table', () => {
|
||||
// expect(wrapper.find('div.gdd-transaction-list').exists()).toBeTruthy()
|
||||
// })
|
||||
|
||||
describe('transaction form', () => {
|
||||
it('steps forward in the dialog', async () => {
|
||||
await wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
|
||||
beforeEach(async () => {
|
||||
wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
|
||||
email: 'user@example.org',
|
||||
amount: 23.45,
|
||||
memo: 'Make the best of it!',
|
||||
})
|
||||
expect(wrapper.findComponent({ name: 'TransactionConfirmation' }).exists()).toBeTruthy()
|
||||
})
|
||||
it('steps forward in the dialog', () => {
|
||||
expect(wrapper.findComponent({ name: 'TransactionConfirmation' }).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -112,18 +112,22 @@ describe('SendOverview', () => {
|
||||
describe('transaction is confirmed and server response is error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
sendMock.mockRejectedValue({ message: 'receiver not found' })
|
||||
sendMock.mockRejectedValue({ message: 'recipiant not known' })
|
||||
await wrapper
|
||||
.findComponent({ name: 'TransactionConfirmation' })
|
||||
.vm.$emit('send-transaction')
|
||||
})
|
||||
|
||||
it('shows the error page', () => {
|
||||
expect(wrapper.find('div.card-body').text()).toContain('form.send_transaction_error')
|
||||
expect(wrapper.find('.test-send_transaction_error').text()).toContain(
|
||||
'form.send_transaction_error',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows recipient not found', () => {
|
||||
expect(wrapper.text()).toContain('transaction.receiverNotFound')
|
||||
expect(wrapper.find('.test-receiver-not-found').text()).toContain(
|
||||
'transaction.receiverNotFound',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-container>
|
||||
<gdd-send :currentTransactionStep="currentTransactionStep">
|
||||
<gdd-send :currentTransactionStep="currentTransactionStep" class="pt-3">
|
||||
<template #transaction-form>
|
||||
<transaction-form :balance="balance" @set-transaction="setTransaction"></transaction-form>
|
||||
</template>
|
||||
<template #transaction-confirmation>
|
||||
<transaction-confirmation
|
||||
:balance="balance"
|
||||
:transactions="transactions"
|
||||
:email="transactionData.email"
|
||||
:amount="transactionData.amount"
|
||||
:memo="transactionData.memo"
|
||||
@ -44,7 +46,6 @@ export default {
|
||||
name: 'SendOverview',
|
||||
components: {
|
||||
GddSend,
|
||||
|
||||
TransactionForm,
|
||||
TransactionConfirmation,
|
||||
TransactionResult,
|
||||
|
||||
@ -2,23 +2,61 @@
|
||||
<div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="display-4 p-4">{{ $t('form.send_check') }}</div>
|
||||
<b-list-group>
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
{{ email }}
|
||||
<b-badge variant="primary" pill>{{ $t('form.recipient') }}</b-badge>
|
||||
</b-list-group-item>
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
{{ $n(amount, 'decimal') }} GDD
|
||||
<b-badge variant="primary" pill>{{ $t('form.amount') }}</b-badge>
|
||||
</b-list-group-item>
|
||||
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||
{{ memo ? memo : '-' }}
|
||||
<b-badge variant="primary" pill>{{ $t('form.message') }}</b-badge>
|
||||
</b-list-group-item>
|
||||
<div class="display-4 pb-4">{{ $t('form.send_check') }}</div>
|
||||
<b-list-group class="">
|
||||
<label class="input-1" for="input-1">{{ $t('form.recipient') }}</label>
|
||||
<b-input-group id="input-group-1" class="borderbottom" size="lg">
|
||||
<b-input-group-prepend class="d-none d-md-block gray-background">
|
||||
<b-icon icon="envelope" class="display-4 m-3"></b-icon>
|
||||
</b-input-group-prepend>
|
||||
<div class="p-3">{{ email }}</div>
|
||||
</b-input-group>
|
||||
<br />
|
||||
<label class="input-2" for="input-2">{{ $t('form.amount') }}</label>
|
||||
<b-input-group id="input-group-2" class="borderbottom" size="lg">
|
||||
<b-input-group-prepend class="p-2 d-none d-md-block gray-background">
|
||||
<div class="m-1 mt-2">GDD</div>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<div class="p-3">{{ $n(amount, 'decimal') }}</div>
|
||||
</b-input-group>
|
||||
|
||||
<br />
|
||||
<label class="input-3" for="input-3">{{ $t('form.message') }}</label>
|
||||
<b-input-group id="input-group-3" class="borderbottom">
|
||||
<b-input-group-prepend class="d-none d-md-block gray-background">
|
||||
<b-icon icon="chat-right-text" class="display-4 m-3 mt-4"></b-icon>
|
||||
</b-input-group-prepend>
|
||||
<div class="p-3">{{ memo ? memo : '-' }}</div>
|
||||
</b-input-group>
|
||||
</b-list-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-container class="bv-example-row mt-3 gray-background p-2">
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
|
||||
<b-col class="text-right">{{ $n(balance, 'decimal') }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">
|
||||
<strong>{{ $t('form.your_amount') }}</strong>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<strong>- {{ $n(amount, 'decimal') }}</strong>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('decay.decay') }}</b-col>
|
||||
<b-col class="text-right" style="border-bottom: double">- {{ $n(decay, 'decimal') }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col class="text-right">~ {{ $n(balance - amount - decay, 'decimal') }}</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
|
||||
<b-row class="mt-4">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('form.cancel') }}</b-button>
|
||||
@ -35,10 +73,27 @@
|
||||
export default {
|
||||
name: 'TransactionConfirmation',
|
||||
props: {
|
||||
balance: { type: Number, default: 0 },
|
||||
email: { type: String, default: '' },
|
||||
amount: { type: Number, default: 0 },
|
||||
memo: { type: String, default: '' },
|
||||
loading: { type: Boolean, default: false },
|
||||
transactions: {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
decay: this.transactions[0].balance,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.gray-background {
|
||||
background-color: #ecebe6a3 !important;
|
||||
}
|
||||
.borderbottom {
|
||||
border-bottom: 1px solid rgb(70, 65, 65);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -20,10 +20,10 @@
|
||||
<div>{{ $t('form.sorry') }}</div>
|
||||
<hr />
|
||||
|
||||
<div>{{ $t('form.send_transaction_error') }}</div>
|
||||
<div class="test-send_transaction_error">{{ $t('form.send_transaction_error') }}</div>
|
||||
|
||||
<hr />
|
||||
<div v-if="errorResult === 'receiver not found'">
|
||||
<div class="test-receiver-not-found" v-if="errorResult === 'recipiant not known'">
|
||||
{{ $t('transaction.receiverNotFound') }}
|
||||
</div>
|
||||
<div v-else>({{ errorResult }})</div>
|
||||
|
||||