mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into apollo_createTransactions
This commit is contained in:
commit
2f67b5f5e2
@ -23,3 +23,7 @@ DB_DATABASE=gradido_community
|
|||||||
#KLICKTIPP_APIKEY_DE=
|
#KLICKTIPP_APIKEY_DE=
|
||||||
#KLICKTIPP_APIKEY_EN=
|
#KLICKTIPP_APIKEY_EN=
|
||||||
#KLICKTIPP=true
|
#KLICKTIPP=true
|
||||||
|
COMMUNITY_NAME=
|
||||||
|
COMMUNITY_URL=
|
||||||
|
COMMUNITY_REGISTER_URL=
|
||||||
|
COMMUNITY_DESCRIPTION=
|
||||||
@ -30,6 +30,14 @@ const klicktipp = {
|
|||||||
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
|
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const community = {
|
||||||
|
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||||
|
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/vue/',
|
||||||
|
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/vue/register',
|
||||||
|
COMMUNITY_DESCRIPTION:
|
||||||
|
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
}
|
||||||
|
|
||||||
const email = {
|
const email = {
|
||||||
EMAIL: process.env.EMAIL === 'true' || false,
|
EMAIL: process.env.EMAIL === 'true' || false,
|
||||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||||
@ -42,6 +50,6 @@ const email = {
|
|||||||
// This is needed by graphql-directive-auth
|
// This is needed by graphql-directive-auth
|
||||||
process.env.APP_SECRET = server.JWT_SECRET
|
process.env.APP_SECRET = server.JWT_SECRET
|
||||||
|
|
||||||
const CONFIG = { ...server, ...database, ...klicktipp, ...email }
|
const CONFIG = { ...server, ...database, ...klicktipp, ...community }
|
||||||
|
|
||||||
export default CONFIG
|
export default CONFIG
|
||||||
|
|||||||
31
backend/src/graphql/model/Community.ts
Normal file
31
backend/src/graphql/model/Community.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class Community {
|
||||||
|
constructor(json?: any) {
|
||||||
|
if (json) {
|
||||||
|
this.id = Number(json.id)
|
||||||
|
this.name = json.name
|
||||||
|
this.url = json.url
|
||||||
|
this.description = json.description
|
||||||
|
this.registerUrl = json.registerUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
name: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
url: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
description: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
registerUrl: string
|
||||||
|
}
|
||||||
49
backend/src/graphql/resolver/CommunityResolver.ts
Normal file
49
backend/src/graphql/resolver/CommunityResolver.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { Resolver, Query } from 'type-graphql'
|
||||||
|
import CONFIG from '../../config'
|
||||||
|
import { Community } from '../model/Community'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class CommunityResolver {
|
||||||
|
@Query(() => Community)
|
||||||
|
async getCommunityInfo(): Promise<Community> {
|
||||||
|
return new Community({
|
||||||
|
name: CONFIG.COMMUNITY_NAME,
|
||||||
|
description: CONFIG.COMMUNITY_DESCRIPTION,
|
||||||
|
url: CONFIG.COMMUNITY_URL,
|
||||||
|
registerUrl: CONFIG.COMMUNITY_REGISTER_URL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => [Community])
|
||||||
|
async communities(): Promise<Community[]> {
|
||||||
|
const communities: Community[] = []
|
||||||
|
|
||||||
|
communities.push(
|
||||||
|
new Community({
|
||||||
|
id: 1,
|
||||||
|
name: 'Gradido Entwicklung',
|
||||||
|
description: 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
url: 'http://localhost/vue/',
|
||||||
|
registerUrl: 'http://localhost/vue/register-community',
|
||||||
|
}),
|
||||||
|
new Community({
|
||||||
|
id: 2,
|
||||||
|
name: 'Gradido Staging',
|
||||||
|
description: 'Der Testserver der Gradido-Akademie.',
|
||||||
|
url: 'https://stage1.gradido.net/vue/',
|
||||||
|
registerUrl: 'https://stage1.gradido.net/vue/register-community',
|
||||||
|
}),
|
||||||
|
new Community({
|
||||||
|
id: 3,
|
||||||
|
name: 'Gradido-Akademie',
|
||||||
|
description: 'Freies Institut für Wirtschaftsbionik.',
|
||||||
|
url: 'https://gradido.net',
|
||||||
|
registerUrl: 'https://gdd1.gradido.com/vue/register-community',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return communities
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,8 +14,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
selected: null,
|
selected: null,
|
||||||
options: [
|
options: [
|
||||||
{ value: 'de', text: this.$t('languages.de') },
|
{ value: 'de', text: this.$t('settings.language.de') },
|
||||||
{ value: 'en', text: this.$t('languages.en') },
|
{ value: 'en', text: this.$t('settings.language.en') },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -105,3 +105,26 @@ export const checkEmailQuery = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const communityInfo = gql`
|
||||||
|
query {
|
||||||
|
getCommunityInfo {
|
||||||
|
name
|
||||||
|
description
|
||||||
|
registerUrl
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const communities = gql`
|
||||||
|
query {
|
||||||
|
communities {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
url
|
||||||
|
description
|
||||||
|
registerUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
"community": "Gemeinschaft",
|
"community": {
|
||||||
"communitys": {
|
"choose-another-community": "Eine andere Gemeinschaft auswählen",
|
||||||
|
"communities": {
|
||||||
"form": {
|
"form": {
|
||||||
"date_period": "Datum / Zeitraum",
|
"date_period": "Datum / Zeitraum",
|
||||||
"hours": "Stunden",
|
"hours": "Stunden",
|
||||||
@ -10,6 +11,13 @@
|
|||||||
"submit": "Einreichen"
|
"submit": "Einreichen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"community": "Gemeinschaft",
|
||||||
|
"continue-to-registration": "Weiter zur Registrierung",
|
||||||
|
"current-community": "Aktuelle Gemeinschaft",
|
||||||
|
"location": "Ort:",
|
||||||
|
"other-communities": "Weitere Gemeinschaften",
|
||||||
|
"switch-to-this-community": "zu dieser Gemeinschaft wechseln"
|
||||||
|
},
|
||||||
"decay": {
|
"decay": {
|
||||||
"calculation_decay": "Berechnung der Vergänglichkeit",
|
"calculation_decay": "Berechnung der Vergänglichkeit",
|
||||||
"calculation_total": "Berechnung der Gesamtsumme",
|
"calculation_total": "Berechnung der Gesamtsumme",
|
||||||
@ -19,7 +27,6 @@
|
|||||||
"decayStart": " - Startblock für Vergänglichkeit am: ",
|
"decayStart": " - Startblock für Vergänglichkeit am: ",
|
||||||
"decay_introduced": "Die Vergänglichkeit wurde Eingeführt am ",
|
"decay_introduced": "Die Vergänglichkeit wurde Eingeführt am ",
|
||||||
"decay_since_last_transaction": "Vergänglichkeit seit der letzten Transaktion",
|
"decay_since_last_transaction": "Vergänglichkeit seit der letzten Transaktion",
|
||||||
"fromCommunity": "Aus der Gemeinschaft",
|
|
||||||
"hours": "Stunden",
|
"hours": "Stunden",
|
||||||
"last_transaction": "Letzte Transaktion",
|
"last_transaction": "Letzte Transaktion",
|
||||||
"minutes": "Minuten",
|
"minutes": "Minuten",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"community": "Community",
|
"community": {
|
||||||
"communitys": {
|
"choose-another-community": "Choose another community",
|
||||||
|
"communities": {
|
||||||
"form": {
|
"form": {
|
||||||
"date_period": "Date / Period",
|
"date_period": "Date / Period",
|
||||||
"hours": "hours",
|
"hours": "hours",
|
||||||
@ -10,6 +11,13 @@
|
|||||||
"submit": "submit"
|
"submit": "submit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"community": "Community",
|
||||||
|
"continue-to-registration": "Continue to registration",
|
||||||
|
"current-community": "Current community",
|
||||||
|
"location": "Location:",
|
||||||
|
"other-communities": "Other communities",
|
||||||
|
"switch-to-this-community": "switch to this community"
|
||||||
|
},
|
||||||
"decay": {
|
"decay": {
|
||||||
"calculation_decay": "Calculation of Decay",
|
"calculation_decay": "Calculation of Decay",
|
||||||
"calculation_total": "Calculation of the grand total",
|
"calculation_total": "Calculation of the grand total",
|
||||||
@ -19,7 +27,6 @@
|
|||||||
"decayStart": " - Starting block for decay at: ",
|
"decayStart": " - Starting block for decay at: ",
|
||||||
"decay_introduced": "Decay was Introduced on",
|
"decay_introduced": "Decay was Introduced on",
|
||||||
"decay_since_last_transaction": "Decay since the last transaction",
|
"decay_since_last_transaction": "Decay since the last transaction",
|
||||||
"fromCommunity": "From the community",
|
|
||||||
"hours": "Hours",
|
"hours": "Hours",
|
||||||
"last_transaction": "Last transaction:",
|
"last_transaction": "Last transaction:",
|
||||||
"minutes": "Minutes",
|
"minutes": "Minutes",
|
||||||
|
|||||||
@ -36,7 +36,7 @@ const routes = [
|
|||||||
path: '/thx/:comingFrom',
|
path: '/thx/:comingFrom',
|
||||||
component: () => import('../views/Pages/thx.vue'),
|
component: () => import('../views/Pages/thx.vue'),
|
||||||
beforeEnter: (to, from, next) => {
|
beforeEnter: (to, from, next) => {
|
||||||
const validFrom = ['password', 'reset', 'register']
|
const validFrom = ['password', 'reset', 'register', 'community']
|
||||||
if (!validFrom.includes(from.path.split('/')[1])) {
|
if (!validFrom.includes(from.path.split('/')[1])) {
|
||||||
next({ path: '/login' })
|
next({ path: '/login' })
|
||||||
} else {
|
} else {
|
||||||
@ -48,6 +48,14 @@ const routes = [
|
|||||||
path: '/password',
|
path: '/password',
|
||||||
component: () => import('../views/Pages/ForgotPassword.vue'),
|
component: () => import('../views/Pages/ForgotPassword.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/register-community',
|
||||||
|
component: () => import('../views/Pages/RegisterCommunity.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/select-community',
|
||||||
|
component: () => import('../views/Pages/RegisterSelectCommunity.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/reset/:optin',
|
path: '/reset/:optin',
|
||||||
component: () => import('../views/Pages/ResetPassword.vue'),
|
component: () => import('../views/Pages/ResetPassword.vue'),
|
||||||
|
|||||||
@ -29,6 +29,9 @@ export const mutations = {
|
|||||||
newsletterState: (state, newsletterState) => {
|
newsletterState: (state, newsletterState) => {
|
||||||
state.newsletterState = newsletterState
|
state.newsletterState = newsletterState
|
||||||
},
|
},
|
||||||
|
community: (state, community) => {
|
||||||
|
state.community = community
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
@ -69,6 +72,7 @@ export const store = new Vuex.Store({
|
|||||||
token: null,
|
token: null,
|
||||||
coinanimation: true,
|
coinanimation: true,
|
||||||
newsletterState: null,
|
newsletterState: null,
|
||||||
|
community: null,
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {},
|
||||||
// Syncronous mutation of the state
|
// Syncronous mutation of the state
|
||||||
|
|||||||
@ -4,14 +4,20 @@ import Login from './Login'
|
|||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
const loginQueryMock = jest.fn().mockResolvedValue({
|
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
login: 'token',
|
getCommunityInfo: {
|
||||||
|
name: 'test12',
|
||||||
|
description: 'test community 12',
|
||||||
|
url: 'http://test12.test12/',
|
||||||
|
registerUrl: 'http://test12.test12/vue/register',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toastErrorMock = jest.fn()
|
const toastErrorMock = jest.fn()
|
||||||
const mockStoreDispach = jest.fn()
|
const mockStoreDispach = jest.fn()
|
||||||
|
const mockStoreCommit = jest.fn()
|
||||||
const mockRouterPush = jest.fn()
|
const mockRouterPush = jest.fn()
|
||||||
const spinnerHideMock = jest.fn()
|
const spinnerHideMock = jest.fn()
|
||||||
const spinnerMock = jest.fn(() => {
|
const spinnerMock = jest.fn(() => {
|
||||||
@ -30,6 +36,15 @@ describe('Login', () => {
|
|||||||
$t: jest.fn((t) => t),
|
$t: jest.fn((t) => t),
|
||||||
$store: {
|
$store: {
|
||||||
dispatch: mockStoreDispach,
|
dispatch: mockStoreDispach,
|
||||||
|
commit: mockStoreCommit,
|
||||||
|
state: {
|
||||||
|
community: {
|
||||||
|
name: 'Gradido Entwicklung',
|
||||||
|
url: 'http://localhost/vue/',
|
||||||
|
registerUrl: 'http://localhost/vue/register',
|
||||||
|
description: 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
$loading: {
|
$loading: {
|
||||||
show: spinnerMock,
|
show: spinnerMock,
|
||||||
@ -41,7 +56,7 @@ describe('Login', () => {
|
|||||||
error: toastErrorMock,
|
error: toastErrorMock,
|
||||||
},
|
},
|
||||||
$apollo: {
|
$apollo: {
|
||||||
query: loginQueryMock,
|
query: apolloQueryMock,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,20 +77,54 @@ describe('Login', () => {
|
|||||||
expect(wrapper.find('div.login-form').exists()).toBeTruthy()
|
expect(wrapper.find('div.login-form').exists()).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('commits the community info to the store', () => {
|
||||||
|
expect(mockStoreCommit).toBeCalledWith('community', {
|
||||||
|
name: 'test12',
|
||||||
|
description: 'test community 12',
|
||||||
|
url: 'http://test12.test12/',
|
||||||
|
registerUrl: 'http://test12.test12/vue/register',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('communities gives back error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apolloQueryMock.mockRejectedValue({
|
||||||
|
message: 'Failed to get communities',
|
||||||
|
})
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts an error message', () => {
|
||||||
|
expect(toastErrorMock).toBeCalledWith('Failed to get communities')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Login header', () => {
|
describe('Login header', () => {
|
||||||
it('has a welcome message', () => {
|
it('has a welcome message', () => {
|
||||||
expect(wrapper.find('div.header').text()).toBe('Gradido site.login.community')
|
expect(wrapper.find('div.header').text()).toBe('Gradido site.login.community')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Community Data', () => {
|
||||||
|
it('has a Community name', () => {
|
||||||
|
expect(wrapper.find('.test-communitydata b').text()).toBe('Gradido Entwicklung')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a Community description', () => {
|
||||||
|
expect(wrapper.find('.test-communitydata p').text()).toBe(
|
||||||
|
'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('links', () => {
|
describe('links', () => {
|
||||||
it('has a link "Forgot Password?"', () => {
|
it('has a link "Forgot Password"', () => {
|
||||||
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual(
|
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual(
|
||||||
'settings.password.forgot_pwd',
|
'settings.password.forgot_pwd',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('links to /password when clicking "Forgot Password?"', () => {
|
it('links to /password when clicking "Forgot Password"', () => {
|
||||||
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/password')
|
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/password')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -86,7 +135,9 @@ describe('Login', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('links to /register when clicking "Create new account"', () => {
|
it('links to /register when clicking "Create new account"', () => {
|
||||||
expect(wrapper.findAllComponents(RouterLinkStub).at(1).props().to).toBe('/register')
|
expect(wrapper.findAllComponents(RouterLinkStub).at(1).props().to).toBe(
|
||||||
|
'/register-community',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -135,10 +186,15 @@ describe('Login', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('form').trigger('submit')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
apolloQueryMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
login: 'token',
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls the API with the given data', () => {
|
it('calls the API with the given data', () => {
|
||||||
expect(loginQueryMock).toBeCalledWith(
|
expect(apolloQueryMock).toBeCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
variables: {
|
variables: {
|
||||||
email: 'user@example.org',
|
email: 'user@example.org',
|
||||||
@ -168,7 +224,7 @@ describe('Login', () => {
|
|||||||
|
|
||||||
describe('login fails', () => {
|
describe('login fails', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loginQueryMock.mockRejectedValue({
|
apolloQueryMock.mockRejectedValue({
|
||||||
message: 'Ouch!',
|
message: 'Ouch!',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -18,9 +18,14 @@
|
|||||||
<b-col lg="5" md="7">
|
<b-col lg="5" md="7">
|
||||||
<b-card no-body class="border-0 mb-0" style="background-color: #ebebeba3 !important">
|
<b-card no-body class="border-0 mb-0" style="background-color: #ebebeba3 !important">
|
||||||
<b-card-body class="p-4">
|
<b-card-body class="p-4">
|
||||||
<div class="text-center text-muted mb-4">
|
<div class="text-center text-muted mb-4 test-communitydata">
|
||||||
<small>{{ $t('login') }}</small>
|
<b>{{ $store.state.community.name }}</b>
|
||||||
|
<p class="text-lead">
|
||||||
|
{{ $store.state.community.description }}
|
||||||
|
</p>
|
||||||
|
{{ $t('login') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
||||||
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
|
<b-form @submit.stop.prevent="handleSubmit(onSubmit)">
|
||||||
<input-email v-model="form.email"></input-email>
|
<input-email v-model="form.email"></input-email>
|
||||||
@ -38,13 +43,17 @@
|
|||||||
</b-card-body>
|
</b-card-body>
|
||||||
</b-card>
|
</b-card>
|
||||||
<b-row class="mt-3">
|
<b-row class="mt-3">
|
||||||
<b-col cols="6">
|
<b-col cols="6" class="text-center text-sm-left col-12 col-sm-6 pb-5">
|
||||||
<router-link to="/password">
|
<router-link to="/password" class="mt-3">
|
||||||
{{ $t('settings.password.forgot_pwd') }}
|
{{ $t('settings.password.forgot_pwd') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="6" class="text-right" v-show="allowRegister">
|
<b-col
|
||||||
<router-link to="/register">
|
cols="6"
|
||||||
|
class="text-center text-sm-right col-12 col-sm-6"
|
||||||
|
v-show="allowRegister"
|
||||||
|
>
|
||||||
|
<router-link to="/register-community" class="mt-3">
|
||||||
{{ $t('site.login.new_wallet') }}
|
{{ $t('site.login.new_wallet') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b-col>
|
</b-col>
|
||||||
@ -58,7 +67,7 @@
|
|||||||
import CONFIG from '../../config'
|
import CONFIG from '../../config'
|
||||||
import InputPassword from '../../components/Inputs/InputPassword'
|
import InputPassword from '../../components/Inputs/InputPassword'
|
||||||
import InputEmail from '../../components/Inputs/InputEmail'
|
import InputEmail from '../../components/Inputs/InputEmail'
|
||||||
import { login } from '../../graphql/queries'
|
import { login, communityInfo } from '../../graphql/queries'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
@ -103,6 +112,21 @@ export default {
|
|||||||
this.$toasted.error(this.$t('error.no-account'))
|
this.$toasted.error(this.$t('error.no-account'))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async onCreated() {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: communityInfo,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.$store.commit('community', result.data.getCommunityInfo)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toasted.error(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.onCreated()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -26,6 +26,12 @@ describe('Register', () => {
|
|||||||
state: {
|
state: {
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
|
community: {
|
||||||
|
name: 'Gradido Entwicklung',
|
||||||
|
url: 'http://localhost/vue/',
|
||||||
|
registerUrl: 'http://localhost/vue/register',
|
||||||
|
description: 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -53,6 +59,18 @@ describe('Register', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Community Data', () => {
|
||||||
|
it('has a Community name?', () => {
|
||||||
|
expect(wrapper.find('.test-communitydata b').text()).toBe('Gradido Entwicklung')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a Community description?', () => {
|
||||||
|
expect(wrapper.find('.test-communitydata p').text()).toBe(
|
||||||
|
'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('links', () => {
|
describe('links', () => {
|
||||||
it('has a link "Back"', () => {
|
it('has a link "Back"', () => {
|
||||||
expect(wrapper.find('.test-button-back').text()).toEqual('back')
|
expect(wrapper.find('.test-button-back').text()).toEqual('back')
|
||||||
@ -127,6 +145,18 @@ describe('Register', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('link Choose another community', () => {
|
||||||
|
it('has a link "Choose another community"', () => {
|
||||||
|
expect(wrapper.find('.test-button-another-community').text()).toEqual(
|
||||||
|
'community.choose-another-community',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('links to /select-community when clicking "Choose another community"', () => {
|
||||||
|
expect(wrapper.find('.test-button-another-community').props().to).toBe('/select-community')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('API calls', () => {
|
describe('API calls', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper.find('#registerFirstname').setValue('Max')
|
wrapper.find('#registerFirstname').setValue('Max')
|
||||||
|
|||||||
@ -13,15 +13,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</b-container>
|
</b-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page content -->
|
<!-- Page content -->
|
||||||
<b-container class="mt--8 p-1">
|
<b-container class="mt--8 p-1">
|
||||||
<!-- Table -->
|
<!-- Table -->
|
||||||
|
|
||||||
<b-row class="justify-content-center">
|
<b-row class="justify-content-center">
|
||||||
<b-col lg="6" md="8">
|
<b-col lg="6" md="8">
|
||||||
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
|
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
|
||||||
<b-card-body class="p-4">
|
<b-card-body class="p-4">
|
||||||
<div class="text-center text-muted mb-4">
|
<div class="text-center text-muted mb-4 test-communitydata">
|
||||||
<small>{{ $t('signup') }}</small>
|
<b>{{ $store.state.community.name }}</b>
|
||||||
|
<p class="text-lead">
|
||||||
|
{{ $store.state.community.description }}
|
||||||
|
</p>
|
||||||
|
<div>{{ $t('signup') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
<validation-observer ref="observer" v-slot="{ handleSubmit }">
|
||||||
@ -118,7 +124,7 @@
|
|||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<b-button class="ml-2 test-button-back" to="/login">
|
<b-button class="test-button-back" variant="outline-secondary" to="/login">
|
||||||
{{ $t('back') }}
|
{{ $t('back') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
||||||
@ -138,6 +144,15 @@
|
|||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
</b-container>
|
||||||
|
<div class="text-center pt-4">
|
||||||
|
<b-button
|
||||||
|
class="test-button-another-community"
|
||||||
|
variant="outline-secondary"
|
||||||
|
to="/select-community"
|
||||||
|
>
|
||||||
|
{{ $t('community.choose-another-community') }}
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
78
frontend/src/views/Pages/RegisterCommunity.spec.js
Normal file
78
frontend/src/views/Pages/RegisterCommunity.spec.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import RegisterCommunity from './RegisterCommunity'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('RegisterCommunity', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$i18n: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
community: {
|
||||||
|
name: 'Gradido Entwicklung',
|
||||||
|
url: 'http://localhost/vue/',
|
||||||
|
registerUrl: 'http://localhost/vue/register',
|
||||||
|
description: 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(RegisterCommunity, { localVue, mocks })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element "#register-community"', () => {
|
||||||
|
expect(wrapper.find('div#register-community').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Displaying the current community info', () => {
|
||||||
|
it('has a current community name', () => {
|
||||||
|
expect(wrapper.find('.header h1').text()).toBe('Gradido Entwicklung')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a current community description', () => {
|
||||||
|
expect(wrapper.find('.header p').text()).toBe(
|
||||||
|
'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a current community location', () => {
|
||||||
|
expect(wrapper.find('.header p.community-location').text()).toBe('http://localhost/vue/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buttons and links', () => {
|
||||||
|
it('has a button "Continue to registration?"', () => {
|
||||||
|
expect(wrapper.findAll('a').at(0).text()).toEqual('community.continue-to-registration')
|
||||||
|
})
|
||||||
|
it('button links to /register when clicking "Continue to registration"', () => {
|
||||||
|
expect(wrapper.findAll('a').at(0).props().to).toBe('/register')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a button "Choose another community?"', () => {
|
||||||
|
expect(wrapper.findAll('a').at(1).text()).toEqual('community.choose-another-community')
|
||||||
|
})
|
||||||
|
it('button links to /select-community when clicking "Choose another community"', () => {
|
||||||
|
expect(wrapper.findAll('a').at(1).props().to).toBe('/select-community')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a button "Back to Login?"', () => {
|
||||||
|
expect(wrapper.findAll('a').at(2).text()).toEqual('back')
|
||||||
|
})
|
||||||
|
it('button links to /login when clicking "Back to Login"', () => {
|
||||||
|
expect(wrapper.findAll('a').at(2).props().to).toBe('/login')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
54
frontend/src/views/Pages/RegisterCommunity.vue
Normal file
54
frontend/src/views/Pages/RegisterCommunity.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div id="register-community">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-3">
|
||||||
|
<b-container>
|
||||||
|
<div class="text-center mb-7 header">
|
||||||
|
<b-row class="justify-content-center">
|
||||||
|
<b-col xl="5" lg="6" md="8" class="px-2">
|
||||||
|
<h1>{{ $store.state.community.name }}</h1>
|
||||||
|
<p class="text-lead">
|
||||||
|
{{ $store.state.community.description }}
|
||||||
|
</p>
|
||||||
|
<p class="text-lead community-location">
|
||||||
|
{{ $store.state.community.url }}
|
||||||
|
</p>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<b-row>
|
||||||
|
<b-col class="text-center">
|
||||||
|
<b-button variant="outline-secondary" to="/register">
|
||||||
|
{{ $t('community.continue-to-registration') }}
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<b-row>
|
||||||
|
<b-col class="text-center">
|
||||||
|
<b-button variant="outline-secondary" to="/select-community">
|
||||||
|
{{ $t('community.choose-another-community') }}
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<hr />
|
||||||
|
<b-row>
|
||||||
|
<b-col class="text-center">
|
||||||
|
<b-button variant="outline-secondary" to="/login">{{ $t('back') }}</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'registerSelectCommunity',
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style></style>
|
||||||
81
frontend/src/views/Pages/RegisterSelectCommunity.spec.js
Normal file
81
frontend/src/views/Pages/RegisterSelectCommunity.spec.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import RegisterSelectCommunity from './RegisterSelectCommunity'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const spinnerHideMock = jest.fn()
|
||||||
|
const spinnerMock = jest.fn(() => {
|
||||||
|
return {
|
||||||
|
hide: spinnerHideMock,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
communities: [
|
||||||
|
{
|
||||||
|
name: 'test1',
|
||||||
|
description: 'description 1',
|
||||||
|
url: 'http://test.test/vue',
|
||||||
|
registerUrl: 'http://localhost/vue/register-community',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const toasterMock = jest.fn()
|
||||||
|
|
||||||
|
describe('RegisterSelectCommunity', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$i18n: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
community: {
|
||||||
|
name: 'Gradido Entwicklung',
|
||||||
|
url: 'http://localhost/vue/',
|
||||||
|
registerUrl: 'http://localhost/vue/register',
|
||||||
|
description: 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
$apollo: {
|
||||||
|
query: apolloQueryMock,
|
||||||
|
},
|
||||||
|
$loading: {
|
||||||
|
show: spinnerMock,
|
||||||
|
},
|
||||||
|
$toasted: {
|
||||||
|
error: toasterMock,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(RegisterSelectCommunity, { localVue, mocks })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element "#register-select-community"', () => {
|
||||||
|
expect(wrapper.find('div#register-select-community').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls the apollo query', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apolloQueryMock.mockRejectedValue({
|
||||||
|
message: 'Wrong thing',
|
||||||
|
})
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toast an error', () => {
|
||||||
|
expect(toasterMock).toBeCalledWith('Wrong thing')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
104
frontend/src/views/Pages/RegisterSelectCommunity.vue
Normal file
104
frontend/src/views/Pages/RegisterSelectCommunity.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div id="register-select-community">
|
||||||
|
<b-container class="text-center">
|
||||||
|
<div class="pb-3">{{ $t('community.current-community') }}</div>
|
||||||
|
|
||||||
|
<div v-if="!pending">
|
||||||
|
<div v-for="community in communities" :key="community.name">
|
||||||
|
<b-card
|
||||||
|
v-if="community.name === $store.state.community.name"
|
||||||
|
class="border-0 mb-0"
|
||||||
|
bg-variant="primary"
|
||||||
|
>
|
||||||
|
<b>{{ community.name }}</b>
|
||||||
|
<br />
|
||||||
|
{{ $store.state.community.description }}
|
||||||
|
<br />
|
||||||
|
<b-button variant="outline-secondary" to="/register">
|
||||||
|
{{ $t('community.continue-to-registration') }}
|
||||||
|
</b-button>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>{{ $t('community.other-communities') }}</div>
|
||||||
|
|
||||||
|
<div v-for="community in communities" :key="community.id" class="pb-3">
|
||||||
|
<b-card v-if="community.name != $store.state.community.name" bg-variant="secondary">
|
||||||
|
<b>{{ community.name }}</b>
|
||||||
|
<br />
|
||||||
|
{{ community.description }}
|
||||||
|
<br />
|
||||||
|
<b>
|
||||||
|
<small>
|
||||||
|
<b-link :href="community.url">{{ community.url }}</b-link>
|
||||||
|
</small>
|
||||||
|
</b>
|
||||||
|
<br />
|
||||||
|
<b-button variant="outline-secondary" :href="community.registerUrl">
|
||||||
|
{{ $t('community.switch-to-this-community') }}
|
||||||
|
</b-button>
|
||||||
|
</b-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center py-lg-4">
|
||||||
|
<b-button variant="outline-secondary" to="/login">{{ $t('back') }}</b-button>
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { communities } from '../../graphql/queries'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'registerSelectCommunity',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
communities: [],
|
||||||
|
pending: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getCommunities() {
|
||||||
|
const loader = this.$loading.show({
|
||||||
|
container: this.$refs.header,
|
||||||
|
})
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: communities,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
this.communities = response.data.communities
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toasted.error(error.message)
|
||||||
|
})
|
||||||
|
loader.hide()
|
||||||
|
this.pending = false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getCommunities()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.alert-light {
|
||||||
|
color: #424543;
|
||||||
|
background-color: #bac1c84a;
|
||||||
|
border-color: #ffffff00;
|
||||||
|
}
|
||||||
|
.bg-primary {
|
||||||
|
background-color: #5e72e41f !important;
|
||||||
|
}
|
||||||
|
.bg-secondary {
|
||||||
|
background-color: #525f7f0f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
background-color: #ffffff5e !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
frontend/src/views/Pages/UserProfile/UserCard.spec.js
Normal file
36
frontend/src/views/Pages/UserProfile/UserCard.spec.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import UserCard from './UserCard'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('UserCard', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$n: jest.fn((n) => String(n)),
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
email: 'user@example.org',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(UserCard, { localVue, mocks })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element ".userdata-card"', () => {
|
||||||
|
expect(wrapper.find('div.userdata-card').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the Div Element "vue-qrcode"', () => {
|
||||||
|
expect(wrapper.find('vue-qrcode'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="userdata-card">
|
||||||
<b-card class="bg-transparent border-0">
|
<b-card class="bg-transparent border-0">
|
||||||
<div class="w-100 text-center">
|
<div class="w-100 text-center">
|
||||||
<vue-qrcode
|
<vue-qrcode
|
||||||
@ -21,10 +22,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="heading">--</span>
|
<span class="heading">--</span>
|
||||||
<span class="description">{{ $t('community') }}</span>
|
<span class="description">{{ $t('community.community') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import VueQrcode from 'vue-qrcode'
|
import VueQrcode from 'vue-qrcode'
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
buildTagFromLanguageString() {
|
buildTagFromLanguageString() {
|
||||||
return 'languages.' + this.$store.state.language
|
return 'settings.language.' + this.$store.state.language
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user