SEBSERV-44 SEBSERV-45 validation on back end
This commit is contained in:
parent
8bbf515717
commit
f4af098a6f
7 changed files with 230 additions and 87 deletions
|
@ -9,7 +9,6 @@
|
||||||
package ch.ethz.seb.sebserver.gui.service.examconfig.impl;
|
package ch.ethz.seb.sebserver.gui.service.examconfig.impl;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import org.eclipse.rap.rwt.RWT;
|
import org.eclipse.rap.rwt.RWT;
|
||||||
import org.eclipse.swt.SWT;
|
import org.eclipse.swt.SWT;
|
||||||
|
@ -85,59 +84,26 @@ public class TextFieldBuilder implements InputFieldBuilder {
|
||||||
errorLabel.setVisible(false);
|
errorLabel.setVisible(false);
|
||||||
errorLabel.setData(RWT.CUSTOM_VARIANT, "error");
|
errorLabel.setData(RWT.CUSTOM_VARIANT, "error");
|
||||||
|
|
||||||
addValueChangeListener(
|
final TextInputField textInputField = new TextInputField(attribute, orientation, text, errorLabel);
|
||||||
text,
|
|
||||||
attribute,
|
|
||||||
orientation,
|
|
||||||
viewContext);
|
|
||||||
|
|
||||||
return new TextInputField(attribute, orientation, text, errorLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addValueChangeListener(
|
|
||||||
final Text control,
|
|
||||||
final ConfigurationAttribute attribute,
|
|
||||||
final Orientation orientation,
|
|
||||||
final ViewContext viewContext) {
|
|
||||||
|
|
||||||
final ValueChangeListener valueListener = viewContext.getValueChangeListener();
|
final ValueChangeListener valueListener = viewContext.getValueChangeListener();
|
||||||
if (attribute.type == AttributeType.INTEGER) {
|
text.addListener(
|
||||||
addNumberCheckListener(control, attribute, s -> Integer.parseInt(s), viewContext);
|
|
||||||
} else if (attribute.type == AttributeType.DECIMAL) {
|
|
||||||
addNumberCheckListener(control, attribute, s -> Double.parseDouble(s), viewContext);
|
|
||||||
} else {
|
|
||||||
control.addListener(
|
|
||||||
SWT.FocusOut,
|
SWT.FocusOut,
|
||||||
event -> valueListener.valueChanged(
|
event -> {
|
||||||
|
textInputField.clearError();
|
||||||
|
valueListener.valueChanged(
|
||||||
viewContext,
|
viewContext,
|
||||||
attribute,
|
attribute,
|
||||||
String.valueOf(control.getText()),
|
String.valueOf(text.getText()),
|
||||||
0));
|
textInputField.listIndex);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addNumberCheckListener(
|
|
||||||
final Text control,
|
|
||||||
final ConfigurationAttribute attribute,
|
|
||||||
final Consumer<String> numberCheck,
|
|
||||||
final ViewContext viewContext) {
|
|
||||||
|
|
||||||
final ValueChangeListener valueListener = viewContext.getValueChangeListener();
|
|
||||||
control.addListener(SWT.FocusOut, event -> {
|
|
||||||
try {
|
|
||||||
final String text = control.getText();
|
|
||||||
numberCheck.accept(text);
|
|
||||||
viewContext.clearError(attribute.id);
|
|
||||||
valueListener.valueChanged(viewContext, attribute, text, 0);
|
|
||||||
} catch (final NumberFormatException e) {
|
|
||||||
viewContext.showError(attribute.id, "Not A Number");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return textInputField;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class TextInputField extends ControlFieldAdapter<Text> {
|
static final class TextInputField extends ControlFieldAdapter<Text> {
|
||||||
|
|
||||||
private String initValue = "";
|
private String initValue = "";
|
||||||
|
private int listIndex = 0;
|
||||||
|
|
||||||
TextInputField(
|
TextInputField(
|
||||||
final ConfigurationAttribute attribute,
|
final ConfigurationAttribute attribute,
|
||||||
|
@ -155,6 +121,7 @@ public class TextFieldBuilder implements InputFieldBuilder {
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(v -> {
|
.map(v -> {
|
||||||
this.initValue = v.value;
|
this.initValue = v.value;
|
||||||
|
this.listIndex = (v.listIndex != null) ? v.listIndex : 0;
|
||||||
setDefaultValue();
|
setDefaultValue();
|
||||||
return this.initValue;
|
return this.initValue;
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValue;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordDynamicSqlSupport;
|
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordDynamicSqlSupport;
|
||||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordMapper;
|
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationAttributeRecordMapper;
|
||||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordMapper;
|
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ConfigurationRecordMapper;
|
||||||
|
@ -156,9 +157,29 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
|
||||||
.flatMap(this::attributeRecord)
|
.flatMap(this::attributeRecord)
|
||||||
.map(attributeRecord -> {
|
.map(attributeRecord -> {
|
||||||
|
|
||||||
|
final Long id;
|
||||||
|
if (data.id == null) {
|
||||||
|
id = this.configurationValueRecordMapper.selectIdsByExample()
|
||||||
|
.where(
|
||||||
|
ConfigurationValueRecordDynamicSqlSupport.configurationId,
|
||||||
|
isEqualTo(data.configurationId))
|
||||||
|
.and(
|
||||||
|
ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId,
|
||||||
|
isEqualTo(data.attributeId))
|
||||||
|
.and(
|
||||||
|
ConfigurationValueRecordDynamicSqlSupport.listIndex,
|
||||||
|
isEqualTo(data.listIndex))
|
||||||
|
.build()
|
||||||
|
.execute()
|
||||||
|
.stream()
|
||||||
|
.collect(Utils.toSingleton());
|
||||||
|
} else {
|
||||||
|
id = data.id;
|
||||||
|
}
|
||||||
|
|
||||||
final boolean bigValue = isBigValue(attributeRecord);
|
final boolean bigValue = isBigValue(attributeRecord);
|
||||||
final ConfigurationValueRecord newRecord = new ConfigurationValueRecord(
|
final ConfigurationValueRecord newRecord = new ConfigurationValueRecord(
|
||||||
data.id,
|
id,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
@ -166,13 +187,8 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
|
||||||
(bigValue) ? null : data.value,
|
(bigValue) ? null : data.value,
|
||||||
(bigValue) ? data.value : null);
|
(bigValue) ? data.value : null);
|
||||||
|
|
||||||
if (data.id != null) {
|
|
||||||
this.configurationValueRecordMapper.updateByPrimaryKeySelective(newRecord);
|
this.configurationValueRecordMapper.updateByPrimaryKeySelective(newRecord);
|
||||||
} else {
|
return this.configurationValueRecordMapper.selectByPrimaryKey(id);
|
||||||
saveByMatch(data, newRecord);
|
|
||||||
}
|
|
||||||
return this.configurationValueRecordMapper.selectByPrimaryKey(data.id);
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.flatMap(ConfigurationValueDAOImpl::toDomainModel)
|
.flatMap(ConfigurationValueDAOImpl::toDomainModel)
|
||||||
.onError(TransactionHandler::rollback);
|
.onError(TransactionHandler::rollback);
|
||||||
|
@ -424,29 +440,4 @@ public class ConfigurationValueDAOImpl implements ConfigurationValueDAO {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Try to identify and save attribute value by configurationId and configurationAttributeId and listIndex
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* @param newRecord
|
|
||||||
* @throws ResourceNotFoundException if no matching attribute value was found */
|
|
||||||
private void saveByMatch(final ConfigurationValue data, final ConfigurationValueRecord newRecord) {
|
|
||||||
|
|
||||||
final Integer execute = this.configurationValueRecordMapper.updateByExample(newRecord)
|
|
||||||
.where(
|
|
||||||
ConfigurationValueRecordDynamicSqlSupport.configurationId,
|
|
||||||
isEqualTo(data.configurationId))
|
|
||||||
.and(
|
|
||||||
ConfigurationValueRecordDynamicSqlSupport.configurationAttributeId,
|
|
||||||
isEqualTo(data.attributeId))
|
|
||||||
.and(
|
|
||||||
ConfigurationValueRecordDynamicSqlSupport.listIndex,
|
|
||||||
isEqualTo(data.listIndex))
|
|
||||||
.build()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (execute == null || execute < 0) {
|
|
||||||
throw new ResourceNotFoundException(EntityType.CONFIGURATION_VALUE, data.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||||
|
|
||||||
|
public interface ConfigurationValueValidator {
|
||||||
|
|
||||||
|
public static final String MESSAGE_VALUE_OBJECT_NAME = "examConfigValue";
|
||||||
|
|
||||||
|
String name();
|
||||||
|
|
||||||
|
boolean validate(
|
||||||
|
ConfigurationValue value,
|
||||||
|
ConfigurationAttribute attribute);
|
||||||
|
|
||||||
|
default void throwValidationError(
|
||||||
|
final ConfigurationValue value,
|
||||||
|
final ConfigurationAttribute attribute) {
|
||||||
|
|
||||||
|
throw new FieldValidationException(
|
||||||
|
attribute.name,
|
||||||
|
this.createErrorMessage(value, attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
default String createErrorMessage(
|
||||||
|
final ConfigurationValue value,
|
||||||
|
final ConfigurationAttribute attribute) {
|
||||||
|
|
||||||
|
return "examConfigValue:" + attribute.name + ":" + name() + ":" + value.listIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValue;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||||
|
|
||||||
|
public interface SebExamConfigService {
|
||||||
|
|
||||||
|
void validate(ConfigurationValue value);
|
||||||
|
|
||||||
|
void validate(ConfigurationTableValue tableValue);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Component
|
||||||
|
@WebServiceProfile
|
||||||
|
public class IntegerTypeValueValidator implements ConfigurationValueValidator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return AttributeType.INTEGER.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validate(
|
||||||
|
final ConfigurationValue value,
|
||||||
|
final ConfigurationAttribute attribute) {
|
||||||
|
|
||||||
|
// if value is not an integer type or another specific validation is defined --> skip
|
||||||
|
if (attribute.type != AttributeType.INTEGER ||
|
||||||
|
StringUtils.isNoneBlank(attribute.validator)) {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(value.value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Integer.parseInt(value.value);
|
||||||
|
return true;
|
||||||
|
} catch (final NumberFormatException nfe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValue;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Service
|
||||||
|
@WebServiceProfile
|
||||||
|
public class SebExamConfigServiceImpl implements SebExamConfigService {
|
||||||
|
|
||||||
|
private final ConfigurationAttributeDAO configurationAttributeDAO;
|
||||||
|
private final Collection<ConfigurationValueValidator> validators;
|
||||||
|
|
||||||
|
protected SebExamConfigServiceImpl(
|
||||||
|
final ConfigurationAttributeDAO configurationAttributeDAO,
|
||||||
|
final Collection<ConfigurationValueValidator> validators) {
|
||||||
|
|
||||||
|
this.configurationAttributeDAO = configurationAttributeDAO;
|
||||||
|
this.validators = validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(final ConfigurationValue value) {
|
||||||
|
Objects.requireNonNull(value);
|
||||||
|
|
||||||
|
final ConfigurationAttribute attribute = this.configurationAttributeDAO.byPK(value.attributeId)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
this.validators
|
||||||
|
.stream()
|
||||||
|
.filter(validator -> !validator.validate(value, attribute))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(validator -> validator.throwValidationError(value, attribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(final ConfigurationTableValue tableValue) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionServic
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
||||||
|
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
|
@ -44,6 +45,7 @@ public class ConfigurationValueController extends EntityController<Configuration
|
||||||
|
|
||||||
private final ConfigurationDAO configurationDAO;
|
private final ConfigurationDAO configurationDAO;
|
||||||
private final ConfigurationValueDAO configurationValueDAO;
|
private final ConfigurationValueDAO configurationValueDAO;
|
||||||
|
private final SebExamConfigService sebExamConfigService;
|
||||||
|
|
||||||
protected ConfigurationValueController(
|
protected ConfigurationValueController(
|
||||||
final AuthorizationService authorization,
|
final AuthorizationService authorization,
|
||||||
|
@ -52,7 +54,8 @@ public class ConfigurationValueController extends EntityController<Configuration
|
||||||
final UserActivityLogDAO userActivityLogDAO,
|
final UserActivityLogDAO userActivityLogDAO,
|
||||||
final PaginationService paginationService,
|
final PaginationService paginationService,
|
||||||
final BeanValidationService beanValidationService,
|
final BeanValidationService beanValidationService,
|
||||||
final ConfigurationDAO configurationDAO) {
|
final ConfigurationDAO configurationDAO,
|
||||||
|
final SebExamConfigService sebExamConfigService) {
|
||||||
|
|
||||||
super(authorization,
|
super(authorization,
|
||||||
bulkActionService,
|
bulkActionService,
|
||||||
|
@ -63,6 +66,7 @@ public class ConfigurationValueController extends EntityController<Configuration
|
||||||
|
|
||||||
this.configurationDAO = configurationDAO;
|
this.configurationDAO = configurationDAO;
|
||||||
this.configurationValueDAO = entityDAO;
|
this.configurationValueDAO = entityDAO;
|
||||||
|
this.sebExamConfigService = sebExamConfigService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -138,17 +142,21 @@ public class ConfigurationValueController extends EntityController<Configuration
|
||||||
_entity = entity;
|
_entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// test either id or (configurationId and attributeId and listIndex) are set
|
// ConfigurationValue identity constraint
|
||||||
if (_entity.id != null ||
|
// test either id or (configurationId and attributeId and listIndex) must be set
|
||||||
(_entity.configurationId != null &&
|
final boolean idSet = _entity.id != null;
|
||||||
|
final boolean idsSet = _entity.configurationId != null &&
|
||||||
_entity.attributeId != null &&
|
_entity.attributeId != null &&
|
||||||
_entity.listIndex != null)) {
|
_entity.listIndex != null;
|
||||||
|
if (!idSet && !idsSet) {
|
||||||
return _entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalAPIArgumentException(
|
throw new IllegalAPIArgumentException(
|
||||||
"Missing some mandatory attributes. Either id must be set or all of configurationId, attributeId and listIndex");
|
"Missing some mandatory attributes. Either id must be set or all of configurationId, attributeId and listIndex");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply field type validation
|
||||||
|
this.sebExamConfigService.validate(_entity);
|
||||||
|
return _entity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue