mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 10:34:15 +00:00
[CST-4884] Bitstream edit form moved inside modal (test WIP)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
import { waitForAsync, ComponentFixture, inject, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -17,18 +17,18 @@ import { SubmissionService } from '../../../../submission.service';
|
|||||||
import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component';
|
import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component';
|
||||||
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
|
import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component';
|
||||||
import {
|
import {
|
||||||
mockGroup,
|
|
||||||
mockSubmissionCollectionId,
|
mockSubmissionCollectionId,
|
||||||
mockSubmissionId,
|
mockSubmissionId,
|
||||||
mockUploadConfigResponse,
|
mockUploadConfigResponse,
|
||||||
mockUploadConfigResponseMetadata,
|
mockUploadConfigResponseMetadata,
|
||||||
mockUploadFiles
|
mockUploadFiles,
|
||||||
|
mockFileFormData,
|
||||||
|
mockSubmissionObject,
|
||||||
} from '../../../../../shared/mocks/submission.mock';
|
} from '../../../../../shared/mocks/submission.mock';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { FormComponent } from '../../../../../shared/form/form.component';
|
import { FormComponent } from '../../../../../shared/form/form.component';
|
||||||
import { FormService } from '../../../../../shared/form/form.service';
|
import { FormService } from '../../../../../shared/form/form.service';
|
||||||
import { getMockFormService } from '../../../../../shared/mocks/form-service.mock';
|
import { getMockFormService } from '../../../../../shared/mocks/form-service.mock';
|
||||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
|
||||||
import { createTestComponent } from '../../../../../shared/testing/utils.test';
|
import { createTestComponent } from '../../../../../shared/testing/utils.test';
|
||||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
|
import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder';
|
||||||
@@ -37,6 +37,9 @@ import { SubmissionJsonPatchOperationsService } from '../../../../../core/submis
|
|||||||
import { SectionUploadService } from '../../section-upload.service';
|
import { SectionUploadService } from '../../section-upload.service';
|
||||||
import { getMockSectionUploadService } from '../../../../../shared/mocks/section-upload.service.mock';
|
import { getMockSectionUploadService } from '../../../../../shared/mocks/section-upload.service.mock';
|
||||||
import { FormFieldMetadataValueObject } from '../../../../../shared/form/builder/models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../../../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
|
import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||||
|
import { dateToISOFormat } from '../../../../../shared/date.util';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
|
const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
|
||||||
add: jasmine.createSpy('add'),
|
add: jasmine.createSpy('add'),
|
||||||
@@ -44,6 +47,8 @@ const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', {
|
|||||||
remove: jasmine.createSpy('remove'),
|
remove: jasmine.createSpy('remove'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formMetadataMock = ['dc.title', 'dc.description'];
|
||||||
|
|
||||||
fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
||||||
|
|
||||||
let comp: SubmissionSectionUploadFileEditComponent;
|
let comp: SubmissionSectionUploadFileEditComponent;
|
||||||
@@ -53,6 +58,8 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
|||||||
let formbuilderService: any;
|
let formbuilderService: any;
|
||||||
let operationsBuilder: any;
|
let operationsBuilder: any;
|
||||||
let operationsService: any;
|
let operationsService: any;
|
||||||
|
let formService: any;
|
||||||
|
let uploadService: any;
|
||||||
|
|
||||||
const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
|
const submissionJsonPatchOperationsServiceStub = new SubmissionJsonPatchOperationsServiceStub();
|
||||||
const submissionId = mockSubmissionId;
|
const submissionId = mockSubmissionId;
|
||||||
@@ -64,6 +71,7 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
|||||||
const fileIndex = '0';
|
const fileIndex = '0';
|
||||||
const fileId = '123456-test-upload';
|
const fileId = '123456-test-upload';
|
||||||
const fileData: any = mockUploadFiles[0];
|
const fileData: any = mockUploadFiles[0];
|
||||||
|
const pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionId, 'files', fileIndex);
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -137,6 +145,8 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
|||||||
formbuilderService = TestBed.inject(FormBuilderService);
|
formbuilderService = TestBed.inject(FormBuilderService);
|
||||||
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
|
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
|
||||||
operationsService = TestBed.inject(SubmissionJsonPatchOperationsService);
|
operationsService = TestBed.inject(SubmissionJsonPatchOperationsService);
|
||||||
|
formService = TestBed.inject(FormService);
|
||||||
|
uploadService = TestBed.inject(SectionUploadService);
|
||||||
|
|
||||||
comp.submissionId = submissionId;
|
comp.submissionId = submissionId;
|
||||||
comp.collectionId = collectionId;
|
comp.collectionId = collectionId;
|
||||||
@@ -146,6 +156,7 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
|||||||
comp.fileIndex = fileIndex;
|
comp.fileIndex = fileIndex;
|
||||||
comp.fileId = fileId;
|
comp.fileId = fileId;
|
||||||
comp.configMetadataForm = configMetadataForm;
|
comp.configMetadataForm = configMetadataForm;
|
||||||
|
comp.formMetadata = formMetadataMock;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -221,6 +232,76 @@ fdescribe('SubmissionSectionUploadFileEditComponent test suite', () => {
|
|||||||
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
|
expect(compAsAny.retrieveValueFromField(field)).toBe('test');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
|
||||||
|
compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent);
|
||||||
|
compAsAny.fileEditComp.formRef = {formGroup: null};
|
||||||
|
compAsAny.fileData = fileData;
|
||||||
|
compAsAny.pathCombiner = pathCombiner;
|
||||||
|
// const event = new Event('click', null);
|
||||||
|
// spyOn(comp, 'switchMode');
|
||||||
|
formService.validateAllFormFields.and.callFake(() => null);
|
||||||
|
formService.isValid.and.returnValue(of(true));
|
||||||
|
formService.getFormData.and.returnValue(of(mockFileFormData));
|
||||||
|
|
||||||
|
const response = [
|
||||||
|
Object.assign(mockSubmissionObject, {
|
||||||
|
sections: {
|
||||||
|
upload: {
|
||||||
|
files: mockUploadFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
operationsService.jsonPatchByResourceID.and.returnValue(of(response));
|
||||||
|
|
||||||
|
const accessConditionsToSave = [
|
||||||
|
{ name: 'openaccess' },
|
||||||
|
{ name: 'lease', endDate: dateToISOFormat('2019-01-16T00:00:00Z') },
|
||||||
|
{ name: 'embargo', startDate: dateToISOFormat('2019-01-16T00:00:00Z') },
|
||||||
|
];
|
||||||
|
comp.saveBitstreamData();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
let path = 'metadata/dc.title';
|
||||||
|
expect(operationsBuilder.add).toHaveBeenCalledWith(
|
||||||
|
pathCombiner.getPath(path),
|
||||||
|
mockFileFormData.metadata['dc.title'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
path = 'metadata/dc.description';
|
||||||
|
expect(operationsBuilder.add).toHaveBeenCalledWith(
|
||||||
|
pathCombiner.getPath(path),
|
||||||
|
mockFileFormData.metadata['dc.description'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
path = 'accessConditions';
|
||||||
|
expect(operationsBuilder.add).toHaveBeenCalledWith(
|
||||||
|
pathCombiner.getPath(path),
|
||||||
|
accessConditionsToSave,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// expect(comp.switchMode).toHaveBeenCalled();
|
||||||
|
expect(uploadService.updateFileData).toHaveBeenCalledWith(submissionId, sectionId, mockUploadFiles[0].uuid, mockUploadFiles[0]);
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not save Bitstream File data properly when form is not valid', fakeAsync(() => {
|
||||||
|
compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent);
|
||||||
|
compAsAny.fileEditComp.formRef = {formGroup: null};
|
||||||
|
compAsAny.pathCombiner = pathCombiner;
|
||||||
|
// const event = new Event('click', null);
|
||||||
|
// spyOn(comp, 'switchMode');
|
||||||
|
formService.validateAllFormFields.and.callFake(() => null);
|
||||||
|
formService.isValid.and.returnValue(of(false));
|
||||||
|
|
||||||
|
// expect(comp.switchMode).not.toHaveBeenCalled();
|
||||||
|
expect(uploadService.updateFileData).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -60,6 +60,97 @@ import { Subscription } from 'rxjs';
|
|||||||
})
|
})
|
||||||
export class SubmissionSectionUploadFileEditComponent implements OnInit {
|
export class SubmissionSectionUploadFileEditComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FormComponent reference
|
||||||
|
*/
|
||||||
|
@ViewChild('formRef') public formRef: FormComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of available access condition
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
public availableAccessConditionOptions: any[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The submission id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public collectionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define if collection access conditions policy type :
|
||||||
|
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
|
||||||
|
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
public collectionPolicyType: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration for the bitstream's metadata form
|
||||||
|
* @type {SubmissionFormsModel}
|
||||||
|
*/
|
||||||
|
public configMetadataForm: SubmissionFormsModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream's metadata data
|
||||||
|
* @type {WorkspaceitemSectionUploadFileObject}
|
||||||
|
*/
|
||||||
|
public fileData: WorkspaceitemSectionUploadFileObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public fileId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream array key
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public fileIndex: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public formId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The section id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public sectionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The submission id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
public submissionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of all available metadata
|
||||||
|
*/
|
||||||
|
formMetadata: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form model
|
||||||
|
* @type {DynamicFormControlModel[]}
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When `true` form controls are deactivated
|
||||||
|
*/
|
||||||
|
isSaving = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [JsonPatchOperationPathCombiner] object
|
||||||
|
* @type {JsonPatchOperationPathCombiner}
|
||||||
|
*/
|
||||||
|
protected pathCombiner: JsonPatchOperationPathCombiner;
|
||||||
|
|
||||||
|
protected subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
@@ -84,94 +175,6 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of available access condition
|
|
||||||
* @type {Array}
|
|
||||||
*/
|
|
||||||
@Input() availableAccessConditionOptions: any[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The submission id
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() collectionId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define if collection access conditions policy type :
|
|
||||||
* POLICY_DEFAULT_NO_LIST : is not possible to define additional access group/s for the single file
|
|
||||||
* POLICY_DEFAULT_WITH_LIST : is possible to define additional access group/s for the single file
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
@Input() collectionPolicyType: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The configuration for the bitstream's metadata form
|
|
||||||
* @type {SubmissionFormsModel}
|
|
||||||
*/
|
|
||||||
@Input() configMetadataForm: SubmissionFormsModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The bitstream's metadata data
|
|
||||||
* @type {WorkspaceitemSectionUploadFileObject}
|
|
||||||
*/
|
|
||||||
@Input() fileData: WorkspaceitemSectionUploadFileObject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The bitstream id
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() fileId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The bitstream array key
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() fileIndex: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The form id
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() formId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The section id
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() sectionId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The submission id
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() submissionId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of all available metadata
|
|
||||||
*/
|
|
||||||
@Input() formMetadata: string[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The [JsonPatchOperationPathCombiner] object
|
|
||||||
* @type {JsonPatchOperationPathCombiner}
|
|
||||||
*/
|
|
||||||
@Input() pathCombiner: JsonPatchOperationPathCombiner;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The FormComponent reference
|
|
||||||
*/
|
|
||||||
@ViewChild('formRef') public formRef: FormComponent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The form model
|
|
||||||
* @type {DynamicFormControlModel[]}
|
|
||||||
*/
|
|
||||||
formModel: DynamicFormControlModel[];
|
|
||||||
|
|
||||||
isSaving = false;
|
|
||||||
|
|
||||||
protected subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize form model values
|
* Initialize form model values
|
||||||
*
|
*
|
||||||
@@ -379,7 +382,7 @@ export class SubmissionSectionUploadFileEditComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Save bitstream metadata
|
* Save bitstream metadata
|
||||||
*/
|
*/
|
||||||
protected saveBitstreamData() {
|
saveBitstreamData() {
|
||||||
// validate form
|
// validate form
|
||||||
this.formService.validateAllFormFields(this.formRef.formGroup);
|
this.formService.validateAllFormFields(this.formRef.formGroup);
|
||||||
const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe(
|
const saveBitstreamDataSubscription = this.formService.isValid(this.formId).pipe(
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
@@ -17,10 +17,8 @@ import { SubmissionJsonPatchOperationsService } from '../../../../core/submissio
|
|||||||
import { SubmissionSectionUploadFileComponent } from './section-upload-file.component';
|
import { SubmissionSectionUploadFileComponent } from './section-upload-file.component';
|
||||||
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub';
|
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub';
|
||||||
import {
|
import {
|
||||||
mockFileFormData,
|
|
||||||
mockSubmissionCollectionId,
|
mockSubmissionCollectionId,
|
||||||
mockSubmissionId,
|
mockSubmissionId,
|
||||||
mockSubmissionObject,
|
|
||||||
mockUploadConfigResponse,
|
mockUploadConfigResponse,
|
||||||
mockUploadFiles
|
mockUploadFiles
|
||||||
} from '../../../../shared/mocks/submission.mock';
|
} from '../../../../shared/mocks/submission.mock';
|
||||||
@@ -32,10 +30,8 @@ import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
|||||||
import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component';
|
import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component';
|
||||||
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||||
import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock';
|
import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock';
|
||||||
import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
|
|
||||||
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
|
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { dateToISOFormat } from '../../../../shared/date.util';
|
|
||||||
|
|
||||||
const configMetadataFormMock = {
|
const configMetadataFormMock = {
|
||||||
rows: [{
|
rows: [{
|
||||||
|
Reference in New Issue
Block a user