Commit ac325495 authored by Jiaan Louw's avatar Jiaan Louw Committed by Jose Ivan Vargas

Update compliance report local resolver to match new format

Applies the new GraphQL format to the local resolver and updates the
compliance report components to the new format.
parent 6eb89613
...@@ -115,21 +115,21 @@ export default { ...@@ -115,21 +115,21 @@ export default {
this.updateUrlQuery({ ...this.urlQuery, sort: this.sortParam }); this.updateUrlQuery({ ...this.urlQuery, sort: this.sortParam });
}, },
toggleDrawer(rows) { toggleDrawer(rows) {
const { mergeRequest, project } = rows[0] || {}; const { mergeRequest } = rows[0] || {};
if (!mergeRequest || this.isCurrentDrawer(mergeRequest)) { if (!mergeRequest || this.isCurrentDrawer(mergeRequest)) {
this.closeDrawer(); this.closeDrawer();
} else { } else {
this.openDrawer(mergeRequest, project); this.openDrawer(mergeRequest);
} }
}, },
isCurrentDrawer(mergeRequest) { isCurrentDrawer(mergeRequest) {
return this.showDrawer && mergeRequest.id === this.drawerMergeRequest.id; return this.showDrawer && mergeRequest.id === this.drawerMergeRequest.id;
}, },
openDrawer(mergeRequest, project) { openDrawer(mergeRequest) {
this.showDrawer = true; this.showDrawer = true;
this.drawerMergeRequest = mergeRequest; this.drawerMergeRequest = mergeRequest;
this.drawerProject = project; this.drawerProject = mergeRequest.project;
}, },
closeDrawer() { closeDrawer() {
this.showDrawer = false; this.showDrawer = false;
...@@ -161,7 +161,7 @@ export default { ...@@ -161,7 +161,7 @@ export default {
}, },
fields: [ fields: [
{ {
key: 'severity', key: 'severityLevel',
label: __('Severity'), label: __('Severity'),
thClass: thWidthClass(10), thClass: thWidthClass(10),
sortable: true, sortable: true,
...@@ -259,8 +259,8 @@ export default { ...@@ -259,8 +259,8 @@ export default {
@row-selected="toggleDrawer" @row-selected="toggleDrawer"
@sort-changed="handleSortChanged" @sort-changed="handleSortChanged"
> >
<template #cell(severity)="{ item: { severity } }"> <template #cell(severityLevel)="{ item: { severityLevel } }">
<severity-badge class="gl-reset-text-align!" :severity="severity" /> <severity-badge class="gl-reset-text-align!" :severity="severityLevel" />
</template> </template>
<template #cell(violationReason)="{ item: { reason, violatingUser } }"> <template #cell(violationReason)="{ item: { reason, violatingUser } }">
<violation-reason :reason="reason" :user="violatingUser" /> <violation-reason :reason="reason" :user="violatingUser" />
......
...@@ -38,11 +38,11 @@ export default { ...@@ -38,11 +38,11 @@ export default {
}, },
computed: { computed: {
defaultStartDate() { defaultStartDate() {
const startDate = this.defaultQuery.createdAfter; const startDate = this.defaultQuery.mergedAfter;
return startDate ? parsePikadayDate(startDate) : getDateInPast(CURRENT_DATE, 30); return startDate ? parsePikadayDate(startDate) : getDateInPast(CURRENT_DATE, 30);
}, },
defaultEndDate() { defaultEndDate() {
const endDate = this.defaultQuery.createdBefore; const endDate = this.defaultQuery.mergedBefore;
return endDate ? parsePikadayDate(endDate) : CURRENT_DATE; return endDate ? parsePikadayDate(endDate) : CURRENT_DATE;
}, },
}, },
...@@ -74,8 +74,8 @@ export default { ...@@ -74,8 +74,8 @@ export default {
}, },
dateRangeChanged({ startDate = this.defaultStartDate, endDate = this.defaultEndDate }) { dateRangeChanged({ startDate = this.defaultStartDate, endDate = this.defaultEndDate }) {
this.updateFilter({ this.updateFilter({
createdAfter: pikadayToString(startDate), mergedAfter: pikadayToString(startDate),
createdBefore: pikadayToString(endDate), mergedBefore: pikadayToString(endDate),
}); });
}, },
updateFilter(query) { updateFilter(query) {
......
<script> <script>
import { MERGE_REQUEST_VIOLATION_REASONS, MERGE_REQUEST_VIOLATION_MESSAGES } from '../../constants'; import { MERGE_REQUEST_VIOLATION_MESSAGES } from '../../constants';
import UserAvatar from '../shared/user_avatar.vue'; import UserAvatar from '../shared/user_avatar.vue';
export default { export default {
...@@ -8,8 +8,9 @@ export default { ...@@ -8,8 +8,9 @@ export default {
}, },
props: { props: {
reason: { reason: {
type: Number, type: String,
required: true, required: true,
validator: (reason) => Object.keys(MERGE_REQUEST_VIOLATION_MESSAGES).includes(reason),
}, },
user: { user: {
type: Object, type: Object,
...@@ -18,11 +19,8 @@ export default { ...@@ -18,11 +19,8 @@ export default {
}, },
}, },
computed: { computed: {
violation() {
return MERGE_REQUEST_VIOLATION_REASONS[this.reason];
},
violationMessage() { violationMessage() {
return MERGE_REQUEST_VIOLATION_MESSAGES[this.violation]; return MERGE_REQUEST_VIOLATION_MESSAGES[this.reason];
}, },
}, },
}; };
......
...@@ -16,27 +16,14 @@ export const GRAPHQL_PAGE_SIZE = 20; ...@@ -16,27 +16,14 @@ export const GRAPHQL_PAGE_SIZE = 20;
export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper'; export const COMPLIANCE_DRAWER_CONTAINER_CLASS = '.content-wrapper';
const VIOLATION_TYPE_APPROVED_BY_AUTHOR = 'approved_by_author'; const APPROVED_BY_COMMITTER = 'APPROVED_BY_COMMITTER';
const VIOLATION_TYPE_APPROVED_BY_COMMITTER = 'approved_by_committer'; const APPROVED_BY_INSUFFICIENT_USERS = 'APPROVED_BY_INSUFFICIENT_USERS';
const VIOLATION_TYPE_APPROVED_BY_INSUFFICIENT_USERS = 'approved_by_insufficient_users'; const APPROVED_BY_MERGE_REQUEST_AUTHOR = 'APPROVED_BY_MERGE_REQUEST_AUTHOR';
export const MERGE_REQUEST_VIOLATION_REASONS = {
0: VIOLATION_TYPE_APPROVED_BY_AUTHOR,
1: VIOLATION_TYPE_APPROVED_BY_COMMITTER,
2: VIOLATION_TYPE_APPROVED_BY_INSUFFICIENT_USERS,
};
export const MERGE_REQUEST_VIOLATION_MESSAGES = { export const MERGE_REQUEST_VIOLATION_MESSAGES = {
[VIOLATION_TYPE_APPROVED_BY_AUTHOR]: s__('ComplianceReport|Approved by author'), [APPROVED_BY_COMMITTER]: s__('ComplianceReport|Approved by committer'),
[VIOLATION_TYPE_APPROVED_BY_COMMITTER]: s__('ComplianceReport|Approved by committer'), [APPROVED_BY_INSUFFICIENT_USERS]: s__('ComplianceReport|Less than 2 approvers'),
[VIOLATION_TYPE_APPROVED_BY_INSUFFICIENT_USERS]: s__('ComplianceReport|Less than 2 approvers'), [APPROVED_BY_MERGE_REQUEST_AUTHOR]: s__('ComplianceReport|Approved by author'),
};
export const MERGE_REQUEST_VIOLATION_SEVERITY_LEVELS = {
1: 'high',
2: 'medium',
3: 'low',
4: 'info',
}; };
export const DEFAULT_SORT = 'SEVERITY_DESC'; export const DEFAULT_SORT = 'SEVERITY_LEVEL_DESC';
...@@ -21,7 +21,7 @@ query getComplianceViolations( ...@@ -21,7 +21,7 @@ query getComplianceViolations(
mergeRequestViolations { mergeRequestViolations {
nodes { nodes {
id id
severity severityLevel
reason reason
violatingUser { violatingUser {
id id
...@@ -42,7 +42,7 @@ query getComplianceViolations( ...@@ -42,7 +42,7 @@ query getComplianceViolations(
avatarUrl avatarUrl
webUrl webUrl
} }
mergedBy { mergeUser {
id id
name name
username username
...@@ -76,24 +76,24 @@ query getComplianceViolations( ...@@ -76,24 +76,24 @@ query getComplianceViolations(
webUrl webUrl
} }
} }
reference ref
fullRef: reference(full: true) fullRef: reference(full: true)
sourceBranch sourceBranch
sourceBranchExists sourceBranchExists
targetBranch targetBranch
targetBranchExists targetBranchExists
} project {
project { id
id avatarUrl
avatarUrl name
name webUrl
webUrl complianceFrameworks {
complianceFrameworks { nodes {
nodes { id
id name
name description
description color
color }
} }
} }
} }
......
import { MERGE_REQUEST_VIOLATION_SEVERITY_LEVELS } from '../constants';
export const mapViolations = (nodes = []) => { export const mapViolations = (nodes = []) => {
return nodes.map((node) => ({ return nodes.map((node) => ({
...node, ...node,
...@@ -8,11 +6,13 @@ export const mapViolations = (nodes = []) => { ...@@ -8,11 +6,13 @@ export const mapViolations = (nodes = []) => {
committers: node.mergeRequest.committers?.nodes || [], committers: node.mergeRequest.committers?.nodes || [],
approvedByUsers: node.mergeRequest.approvedBy?.nodes || [], approvedByUsers: node.mergeRequest.approvedBy?.nodes || [],
participants: node.mergeRequest.participants?.nodes || [], participants: node.mergeRequest.participants?.nodes || [],
// TODO: Once the legacy dashboard is removed (https://gitlab.com/gitlab-org/gitlab/-/issues/346266) we can update the drawer to use the new attributes and remove these 2 mappings
reference: node.mergeRequest.ref,
mergedBy: node.mergeRequest.mergeUser,
project: {
...node.mergeRequest.project,
complianceFramework: node.mergeRequest.project?.complianceFrameworks?.nodes[0] || null,
},
}, },
project: {
...node.project,
complianceFramework: node.project?.complianceFrameworks?.nodes[0] || null,
},
severity: MERGE_REQUEST_VIOLATION_SEVERITY_LEVELS[node.severity],
})); }));
}; };
...@@ -15,8 +15,8 @@ export default { ...@@ -15,8 +15,8 @@ export default {
{ {
__typename: 'MergeRequestViolation', __typename: 'MergeRequestViolation',
id: 1, id: 1,
severity: 1, severityLevel: 'HIGH',
reason: 1, reason: 'APPROVED_BY_COMMITTER',
violatingUser: { violatingUser: {
__typename: 'Violator', __typename: 'Violator',
id: 50, id: 50,
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon', 'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6', webUrl: 'https://gdk.localhost:3443/user6',
}, },
mergedBy: { mergeUser: {
__typename: 'MergedBy', __typename: 'MergedBy',
id: 50, id: 50,
name: 'John Doe6', name: 'John Doe6',
...@@ -93,37 +93,37 @@ export default { ...@@ -93,37 +93,37 @@ export default {
], ],
}, },
fullRef: 'gitlab-shell!1', fullRef: 'gitlab-shell!1',
reference: '!1', ref: '!1',
sourceBranch: 'ut-171ad4e263', sourceBranch: 'ut-171ad4e263',
sourceBranchExists: false, sourceBranchExists: false,
targetBranch: 'master', targetBranch: 'master',
targetBranchExists: true, targetBranchExists: true,
}, project: {
project: { __typename: 'Project',
__typename: 'Project', id: 1,
id: 1, avatarUrl: null,
avatarUrl: null, name: 'Gitlab Shell',
name: 'Gitlab Shell', webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-shell',
webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-shell', complianceFrameworks: {
complianceFrameworks: { __typename: 'ComplianceFrameworks',
__typename: 'ComplianceFrameworks', nodes: [
nodes: [ {
{ __typename: 'ComplianceFrameworks',
__typename: 'ComplianceFrameworks', id: 1,
id: 1, name: 'GDPR',
name: 'GDPR', description: 'General Data Protection Regulation',
description: 'General Data Protection Regulation', color: '#009966',
color: '#009966', },
}, ],
], },
}, },
}, },
}, },
{ {
__typename: 'MergeRequestViolation', __typename: 'MergeRequestViolation',
id: 2, id: 2,
severity: 2, severityLevel: 'HIGH',
reason: 2, reason: 'APPROVED_BY_INSUFFICIENT_USERS',
violatingUser: { violatingUser: {
__typename: 'Violator', __typename: 'Violator',
id: 50, id: 50,
...@@ -149,7 +149,7 @@ export default { ...@@ -149,7 +149,7 @@ export default {
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon', 'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6', webUrl: 'https://gdk.localhost:3443/user6',
}, },
mergedBy: { mergeUser: {
__typename: 'MergedBy', __typename: 'MergedBy',
id: 50, id: 50,
name: 'John Doe6', name: 'John Doe6',
...@@ -191,29 +191,29 @@ export default { ...@@ -191,29 +191,29 @@ export default {
], ],
}, },
fullRef: 'gitlab-test!2', fullRef: 'gitlab-test!2',
reference: '!2', ref: '!2',
sourceBranch: 'ut-171ad4e264', sourceBranch: 'ut-171ad4e264',
sourceBranchExists: false, sourceBranchExists: false,
targetBranch: 'master', targetBranch: 'master',
targetBranchExists: true, targetBranchExists: true,
}, project: {
project: { __typename: 'Project',
__typename: 'Project', id: 2,
id: 2, avatarUrl: null,
avatarUrl: null, name: 'Gitlab Test',
name: 'Gitlab Test', webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-test',
webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-test', complianceFrameworks: {
complianceFrameworks: { __typename: 'ComplianceFrameworks',
__typename: 'ComplianceFrameworks', nodes: [
nodes: [ {
{ __typename: 'ComplianceFrameworks',
__typename: 'ComplianceFrameworks', id: 2,
id: 2, name: 'SOX',
name: 'SOX', description: 'A framework',
description: 'A framework', color: '#00FF00',
color: '#00FF00', },
}, ],
], },
}, },
}, },
}, },
......
...@@ -28,7 +28,7 @@ describe('MergeRequestDrawer component', () => { ...@@ -28,7 +28,7 @@ describe('MergeRequestDrawer component', () => {
targetBranch: null, targetBranch: null,
targetBranchUri: null, targetBranchUri: null,
}, },
project: defaultData.project, project: defaultData.mergeRequest.project,
}; };
const findTitle = () => wrapper.findByTestId('dashboard-drawer-title'); const findTitle = () => wrapper.findByTestId('dashboard-drawer-title');
......
...@@ -30,12 +30,12 @@ describe('ComplianceReport component', () => { ...@@ -30,12 +30,12 @@ describe('ComplianceReport component', () => {
const mergeCommitsCsvExportPath = '/csv'; const mergeCommitsCsvExportPath = '/csv';
const groupPath = 'group-path'; const groupPath = 'group-path';
const createdAfter = '2021-11-16'; const mergedAfter = '2021-11-16';
const createdBefore = '2021-12-15'; const mergedBefore = '2021-12-15';
const defaultQuery = { const defaultQuery = {
projectIds: ['20'], projectIds: ['20'],
createdAfter, mergedAfter,
createdBefore, mergedBefore,
sort: DEFAULT_SORT, sort: DEFAULT_SORT,
}; };
const mockGraphQlError = new Error('GraphQL networkError'); const mockGraphQlError = new Error('GraphQL networkError');
...@@ -237,9 +237,9 @@ describe('ComplianceReport component', () => { ...@@ -237,9 +237,9 @@ describe('ComplianceReport component', () => {
}); });
it('renders the violation severity badge', () => { it('renders the violation severity badge', () => {
const { severity } = mapViolations(mockResolver().mergeRequestViolations.nodes)[0]; const { severityLevel } = mapViolations(mockResolver().mergeRequestViolations.nodes)[0];
expect(findSeverityBadge().props()).toStrictEqual({ severity }); expect(findSeverityBadge().props()).toStrictEqual({ severity: severityLevel });
}); });
it('renders the violation reason', () => { it('renders the violation reason', () => {
...@@ -289,7 +289,7 @@ describe('ComplianceReport component', () => { ...@@ -289,7 +289,7 @@ describe('ComplianceReport component', () => {
stripTypenames(drawerData.mergeRequest), stripTypenames(drawerData.mergeRequest),
); );
expect(findMergeRequestDrawer().props('project')).toStrictEqual( expect(findMergeRequestDrawer().props('project')).toStrictEqual(
stripTypenames(drawerData.project), stripTypenames(drawerData.mergeRequest.project),
); );
}); });
...@@ -313,7 +313,7 @@ describe('ComplianceReport component', () => { ...@@ -313,7 +313,7 @@ describe('ComplianceReport component', () => {
stripTypenames(drawerData.mergeRequest), stripTypenames(drawerData.mergeRequest),
); );
expect(findMergeRequestDrawer().props('project')).toStrictEqual( expect(findMergeRequestDrawer().props('project')).toStrictEqual(
stripTypenames(drawerData.project), stripTypenames(drawerData.mergeRequest.project),
); );
}); });
}); });
...@@ -335,7 +335,7 @@ describe('ComplianceReport component', () => { ...@@ -335,7 +335,7 @@ describe('ComplianceReport component', () => {
}); });
describe('when the filters changed', () => { describe('when the filters changed', () => {
const query = { createdAfter, createdBefore, projectIds: [1, 2, 3] }; const query = { mergedAfter, mergedBefore, projectIds: [1, 2, 3] };
beforeEach(() => { beforeEach(() => {
return findViolationFilter().vm.$emit('filters-changed', query); return findViolationFilter().vm.$emit('filters-changed', query);
......
...@@ -21,8 +21,8 @@ describe('ViolationFilter component', () => { ...@@ -21,8 +21,8 @@ describe('ViolationFilter component', () => {
const startDate = getDateInPast(CURRENT_DATE, 20); const startDate = getDateInPast(CURRENT_DATE, 20);
const endDate = getDateInPast(CURRENT_DATE, 4); const endDate = getDateInPast(CURRENT_DATE, 4);
const dateRangeQuery = { const dateRangeQuery = {
createdAfter: pikadayToString(startDate), mergedAfter: pikadayToString(startDate),
createdBefore: pikadayToString(endDate), mergedBefore: pikadayToString(endDate),
}; };
const defaultProjects = createDefaultProjects(2); const defaultProjects = createDefaultProjects(2);
const projectsResponse = createDefaultProjectsResponse(defaultProjects); const projectsResponse = createDefaultProjectsResponse(defaultProjects);
...@@ -119,7 +119,7 @@ describe('ViolationFilter component', () => { ...@@ -119,7 +119,7 @@ describe('ViolationFilter component', () => {
}); });
describe('with a default query', () => { describe('with a default query', () => {
const defaultQuery = { projectIds, createdAfter: '2022-01-01', createdBefore: '2022-01-31' }; const defaultQuery = { projectIds, mergedAfter: '2022-01-01', mergedBefore: '2022-01-31' };
beforeEach(() => { beforeEach(() => {
createComponent({ defaultQuery }); createComponent({ defaultQuery });
......
...@@ -2,18 +2,14 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,18 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import ViolationReason from 'ee/compliance_dashboard/components/violations/reason.vue'; import ViolationReason from 'ee/compliance_dashboard/components/violations/reason.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import UserAvatar from 'ee/compliance_dashboard/components/shared/user_avatar.vue'; import UserAvatar from 'ee/compliance_dashboard/components/shared/user_avatar.vue';
import { import { MERGE_REQUEST_VIOLATION_MESSAGES } from 'ee/compliance_dashboard/constants';
MERGE_REQUEST_VIOLATION_MESSAGES,
MERGE_REQUEST_VIOLATION_REASONS,
} from 'ee/compliance_dashboard/constants';
import { createUser } from '../../mock_data'; import { createUser } from '../../mock_data';
describe('ViolationReason component', () => { describe('ViolationReason component', () => {
let wrapper; let wrapper;
const user = convertObjectPropsToCamelCase(createUser(1)); const user = convertObjectPropsToCamelCase(createUser(1));
const reasons = Object.keys(MERGE_REQUEST_VIOLATION_MESSAGES);
const getViolationMessage = (reason) =>
MERGE_REQUEST_VIOLATION_MESSAGES[MERGE_REQUEST_VIOLATION_REASONS[reason]];
const findAvatar = () => wrapper.findComponent(UserAvatar); const findAvatar = () => wrapper.findComponent(UserAvatar);
const createComponent = (propsData = {}) => { const createComponent = (propsData = {}) => {
...@@ -25,30 +21,22 @@ describe('ViolationReason component', () => { ...@@ -25,30 +21,22 @@ describe('ViolationReason component', () => {
}); });
describe('violation message', () => { describe('violation message', () => {
it.each` it.each(reasons)('renders the violation message for the reason %s', (reason) => {
reason | message createComponent({ reason });
${0} | ${getViolationMessage(0)}
${1} | ${getViolationMessage(1)} expect(wrapper.text()).toContain(MERGE_REQUEST_VIOLATION_MESSAGES[reason]);
${2} | ${getViolationMessage(2)} });
`(
'renders the violation message "$message" for the reason code $reason',
({ reason, message }) => {
createComponent({ reason });
expect(wrapper.text()).toContain(message);
},
);
}); });
describe('violation user', () => { describe('violation user', () => {
it('does not render a user avatar by default', () => { it('does not render a user avatar by default', () => {
createComponent({ reason: 0 }); createComponent({ reason: reasons[0] });
expect(findAvatar().exists()).toBe(false); expect(findAvatar().exists()).toBe(false);
}); });
it('renders a user avatar when the user prop is set', () => { it('renders a user avatar when the user prop is set', () => {
createComponent({ reason: 0, user }); createComponent({ reason: reasons[0], user });
expect(findAvatar().props('user')).toBe(user); expect(findAvatar().props('user')).toBe(user);
}); });
......
import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers'; import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers';
import resolvers from 'ee/compliance_dashboard/graphql/resolvers'; import resolvers from 'ee/compliance_dashboard/graphql/resolvers';
import { MERGE_REQUEST_VIOLATION_SEVERITY_LEVELS } from 'ee/compliance_dashboard/constants';
describe('mapViolations', () => { describe('mapViolations', () => {
const mockViolations = resolvers.Query.group().mergeRequestViolations.nodes; const mockViolations = resolvers.Query.group().mergeRequestViolations.nodes;
const severityLevels = Object.keys(MERGE_REQUEST_VIOLATION_SEVERITY_LEVELS).map(Number);
it.each(severityLevels)( it('returns the expected result', () => {
'maps to the expected severity level when the violation severity number is %s', const { mergeRequest } = mapViolations([{ ...mockViolations[0] }])[0];
(severity) => {
const { severity: severityLevel } = mapViolations([{ ...mockViolations[0], severity }])[0];
expect(severityLevel).toBe(MERGE_REQUEST_VIOLATION_SEVERITY_LEVELS[severity]); expect(mergeRequest).toMatchObject({
}, reference: mergeRequest.ref,
); mergedBy: mergeRequest.mergeUser,
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment