mirror of
https://github.com/sismics/docs.git
synced 2024-11-25 15:17:57 +01:00
#18: Add/update/delete groups
This commit is contained in:
parent
21b3ba2bf6
commit
689a4e6aae
@ -48,6 +48,23 @@ public class GroupDao {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a group by ID.
|
||||
*
|
||||
* @param id Group ID
|
||||
* @return Group
|
||||
*/
|
||||
public Group getActiveById(String id) {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
Query q = em.createQuery("select g from Group g where g.id = :id and g.deleteDate is null");
|
||||
q.setParameter("id", id);
|
||||
try {
|
||||
return (Group) q.getSingleResult();
|
||||
} catch (NoResultException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new group.
|
||||
*
|
||||
@ -99,6 +116,10 @@ public class GroupDao {
|
||||
q.setParameter("dateNow", dateNow);
|
||||
q.executeUpdate();
|
||||
|
||||
q = em.createQuery("update Group g set g.parentId = null where g.parentId = :groupId and g.deleteDate is null");
|
||||
q.setParameter("groupId", groupDb.getId());
|
||||
q.executeUpdate();
|
||||
|
||||
// Create audit log
|
||||
AuditLogUtil.create(groupDb, AuditLogType.DELETE, userId);
|
||||
}
|
||||
@ -232,5 +253,30 @@ public class GroupDao {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a group.
|
||||
*
|
||||
* @param group Group to update
|
||||
* @param userId User ID
|
||||
* @return Updated group
|
||||
*/
|
||||
public Group update(Group group, String userId) {
|
||||
EntityManager em = ThreadLocalContext.get().getEntityManager();
|
||||
|
||||
// Get the group
|
||||
Query q = em.createQuery("select g from Group g where g.id = :id and g.deleteDate is null");
|
||||
q.setParameter("id", group.getId());
|
||||
Group groupFromDb = (Group) q.getSingleResult();
|
||||
|
||||
// Update the group
|
||||
groupFromDb.setName(group.getName());
|
||||
groupFromDb.setParentId(group.getParentId());
|
||||
|
||||
// Create audit log
|
||||
AuditLogUtil.create(groupFromDb, AuditLogType.UPDATE, userId);
|
||||
|
||||
return groupFromDb;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import javax.json.JsonObjectBuilder;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.FormParam;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
@ -83,6 +84,87 @@ public class GroupResource extends BaseResource {
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a group.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
@POST
|
||||
@Path("{groupName: [a-zA-Z0-9_]+}")
|
||||
public Response update(@PathParam("groupName") String groupName,
|
||||
@FormParam("parent") String parentName,
|
||||
@FormParam("name") String name) {
|
||||
if (!authenticate()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
checkBaseFunction(BaseFunction.ADMIN);
|
||||
|
||||
// Validate input
|
||||
name = ValidationUtil.validateLength(name, "name", 1, 50, false);
|
||||
ValidationUtil.validateAlphanumeric(name, "name");
|
||||
|
||||
// Get the group (by its old name)
|
||||
GroupDao groupDao = new GroupDao();
|
||||
Group group = groupDao.getActiveByName(groupName);
|
||||
if (group == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
// Avoid duplicates
|
||||
Group existingGroup = groupDao.getActiveByName(name);
|
||||
if (existingGroup != null && existingGroup.getId() != group.getId()) {
|
||||
throw new ClientException("GroupAlreadyExists", MessageFormat.format("This group already exists: {0}", name));
|
||||
}
|
||||
|
||||
// Validate parent
|
||||
String parentId = null;
|
||||
if (!Strings.isNullOrEmpty(parentName)) {
|
||||
Group parentGroup = groupDao.getActiveByName(parentName);
|
||||
if (parentGroup == null) {
|
||||
throw new ClientException("ParentGroupNotFound", MessageFormat.format("This group does not exists: {0}", parentName));
|
||||
}
|
||||
parentId = parentGroup.getId();
|
||||
}
|
||||
|
||||
// Update the group
|
||||
groupDao.update(group.setName(name)
|
||||
.setParentId(parentId), principal.getId());
|
||||
|
||||
// Always return OK
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("status", "ok");
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a group.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
@DELETE
|
||||
@Path("{groupName: [a-zA-Z0-9_]+}")
|
||||
public Response delete(@PathParam("groupName") String groupName) {
|
||||
if (!authenticate()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
checkBaseFunction(BaseFunction.ADMIN);
|
||||
|
||||
// Get the group
|
||||
GroupDao groupDao = new GroupDao();
|
||||
Group group = groupDao.getActiveByName(groupName);
|
||||
if (group == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
// Delete the group
|
||||
groupDao.delete(group.getId(), principal.getId());
|
||||
|
||||
// Always return OK
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("status", "ok");
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to a group.
|
||||
*
|
||||
@ -213,4 +295,40 @@ public class GroupResource extends BaseResource {
|
||||
.add("groups", groups);
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group.
|
||||
*
|
||||
* @param groupName Group name
|
||||
* @return Response
|
||||
*/
|
||||
@GET
|
||||
@Path("{groupName: [a-zA-Z0-9_]+}")
|
||||
public Response get(@PathParam("groupName") String groupName) {
|
||||
if (!authenticate()) {
|
||||
throw new ForbiddenClientException();
|
||||
}
|
||||
checkBaseFunction(BaseFunction.ADMIN);
|
||||
|
||||
// Get the group
|
||||
GroupDao groupDao = new GroupDao();
|
||||
Group group = groupDao.getActiveByName(groupName);
|
||||
if (group == null) {
|
||||
return Response.status(Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
// Build the response
|
||||
JsonObjectBuilder response = Json.createObjectBuilder()
|
||||
.add("name", group.getName());
|
||||
|
||||
// Get the parent
|
||||
if (group.getParentId() != null) {
|
||||
Group parentGroup = groupDao.getActiveById(group.getParentId());
|
||||
response.add("parent", parentGroup.getName());
|
||||
}
|
||||
|
||||
// TODO Add members
|
||||
|
||||
return Response.ok().entity(response.build()).build();
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,33 @@ angular.module('docs',
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings.group', {
|
||||
url: '/group',
|
||||
views: {
|
||||
'settings': {
|
||||
templateUrl: 'partial/docs/settings.group.html',
|
||||
controller: 'SettingsGroup'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings.group.edit', {
|
||||
url: '/edit/:name',
|
||||
views: {
|
||||
'group': {
|
||||
templateUrl: 'partial/docs/settings.group.edit.html',
|
||||
controller: 'SettingsGroupEdit'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings.group.add', {
|
||||
url: '/add',
|
||||
views: {
|
||||
'group': {
|
||||
templateUrl: 'partial/docs/settings.group.edit.html',
|
||||
controller: 'SettingsGroupEdit'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings.vocabulary', {
|
||||
url: '/vocabulary',
|
||||
views: {
|
||||
|
@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Settings group page controller.
|
||||
*/
|
||||
angular.module('docs').controller('SettingsGroup', function($scope, $state, Restangular) {
|
||||
/**
|
||||
* Load groups from server.
|
||||
*/
|
||||
$scope.loadGroups = function() {
|
||||
Restangular.one('group').get().then(function(data) {
|
||||
$scope.groups = data.groups;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loadGroups();
|
||||
|
||||
/**
|
||||
* Edit a group.
|
||||
*/
|
||||
$scope.editGroup = function(group) {
|
||||
$state.go('settings.group.edit', { name: group.name });
|
||||
};
|
||||
});
|
@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Settings group edition page controller.
|
||||
*/
|
||||
angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog, $state, $stateParams, Restangular, $q) {
|
||||
/**
|
||||
* Returns true if in edit mode (false in add mode).
|
||||
*/
|
||||
$scope.isEdit = function() {
|
||||
return $stateParams.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* In edit mode, load the current group.
|
||||
*/
|
||||
if ($scope.isEdit()) {
|
||||
Restangular.one('group', $stateParams.name).get().then(function(data) {
|
||||
$scope.group = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current group.
|
||||
*/
|
||||
$scope.edit = function() {
|
||||
var promise = null;
|
||||
var group = angular.copy($scope.group);
|
||||
|
||||
if ($scope.isEdit()) {
|
||||
promise = Restangular
|
||||
.one('group', $stateParams.name)
|
||||
.post('', group);
|
||||
} else {
|
||||
promise = Restangular
|
||||
.one('group')
|
||||
.put(group);
|
||||
}
|
||||
|
||||
promise.then(function() {
|
||||
$scope.loadGroups();
|
||||
if ($scope.isEdit()) {
|
||||
$state.go('settings.group');
|
||||
} else {
|
||||
// Go to edit this group to add members
|
||||
$state.go('settings.group.edit', { name: group.name });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the current group.
|
||||
*/
|
||||
$scope.remove = function () {
|
||||
var title = 'Delete group';
|
||||
var msg = 'Do you really want to delete this group?';
|
||||
var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
|
||||
|
||||
$dialog.messageBox(title, msg, btns, function(result) {
|
||||
if (result == 'ok') {
|
||||
Restangular.one('group', $stateParams.name).remove().then(function() {
|
||||
$scope.loadGroups();
|
||||
$state.go('settings.group');
|
||||
}, function () {
|
||||
$state.go('settings.group');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a promise for typeahead group.
|
||||
*/
|
||||
$scope.getGroupTypeahead = function($viewValue) {
|
||||
var deferred = $q.defer();
|
||||
Restangular.one('group')
|
||||
.getList('', {
|
||||
sort_column: 1,
|
||||
asc: true
|
||||
}).then(function(data) {
|
||||
deferred.resolve(_.pluck(_.filter(data.groups, function(group) {
|
||||
return group.name.indexOf($viewValue) !== -1;
|
||||
}), 'name'));
|
||||
});
|
||||
return deferred.promise;
|
||||
};
|
||||
});
|
@ -31,12 +31,12 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
|
||||
|
||||
if ($scope.isEdit()) {
|
||||
promise = Restangular
|
||||
.one('user', $stateParams.username)
|
||||
.post('', user);
|
||||
.one('user', $stateParams.username)
|
||||
.post('', user);
|
||||
} else {
|
||||
promise = Restangular
|
||||
.one('user')
|
||||
.put(user);
|
||||
.one('user')
|
||||
.put(user);
|
||||
}
|
||||
|
||||
promise.then(function() {
|
||||
|
@ -60,6 +60,8 @@
|
||||
<script src="app/docs/controller/SettingsLog.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/SettingsUser.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/SettingsUserEdit.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/SettingsGroup.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/SettingsGroupEdit.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/SettingsVocabulary.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/User.js" type="text/javascript"></script>
|
||||
<script src="app/docs/controller/UserProfile.js" type="text/javascript"></script>
|
||||
@ -109,7 +111,7 @@
|
||||
<a href="#/tag"><span class="glyphicon glyphicon-tags"></span> Tags</a>
|
||||
</li>
|
||||
<li ng-class="{active: $uiRoute}" ui-route="/user.*">
|
||||
<a href="#/user"><span class="glyphicon glyphicon-user"></span> Users</a>
|
||||
<a href="#/user"><span class="glyphicon glyphicon-user"></span> Users & Groups</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
<img src="img/loader.gif" ng-show="!group && isEdit()" />
|
||||
|
||||
<div ng-show="group || !isEdit()">
|
||||
<h2 ng-show="isEdit()">Edit
|
||||
<small>"{{ group.name }}"</small>
|
||||
</h2>
|
||||
<h2 ng-show="!isEdit()">Add
|
||||
<small>group</small>
|
||||
</h2>
|
||||
<form class="form-horizontal" name="editGroupForm" novalidate>
|
||||
<div class="form-group" ng-class="{ 'has-error': !editGroupForm.name.$valid, success: editGroupForm.name.$valid }">
|
||||
<label class="col-sm-2 control-label" for="inputName">Name</label>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input name="name" type="text" id="inputName" required class="form-control"
|
||||
ng-minlength="3" ng-maxlength="50" placeholder="Name" ng-model="group.name"/>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<span class="help-block" ng-show="editGroupForm.name.$error.required">Required</span>
|
||||
<span class="help-block" ng-show="editGroupForm.name.$error.minlength">Too short</span>
|
||||
<span class="help-block" ng-show="editGroupForm.name.$error.maxlength">Too long</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label" for="inputName">Parent group</label>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<input name="name" type="text" id="inputParent" class="form-control" autocomplete="off"
|
||||
placeholder="Type a group name" ng-model="group.parent"
|
||||
typeahead="group for group in getGroupTypeahead($viewValue) | filter: $viewValue"
|
||||
typeahead-wait-ms="200" typeahead-editable="false" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editGroupForm.$valid">
|
||||
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'Edit' : 'Add' }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit()">
|
||||
<span class="glyphicon glyphicon-trash"></span> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h3>Members</h3>
|
||||
|
||||
</div>
|
@ -0,0 +1,23 @@
|
||||
<h1>Groups <small>management</small> <a class="btn btn-primary" href="#/settings/group/add">Add</a></h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 well">
|
||||
<table class="table table-striped table-hover table-users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in groups | orderBy: 'name'" ng-click="editGroup(group)"
|
||||
ng-class="{ active: $stateParams.name == group.name }">
|
||||
<td>{{ group.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div ui-view="group"></div>
|
||||
</div>
|
||||
</div>
|
@ -12,6 +12,7 @@
|
||||
<div class="panel-heading" ng-show="isAdmin"><strong>General settings</strong></div>
|
||||
<ul class="list-group">
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/user.*" href="#/settings/user">Users</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/group.*" href="#/settings/group">Groups</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/vocabulary.*" href="#/settings/vocabulary">Vocabularies</a>
|
||||
<a class="list-group-item" ng-show="isAdmin" ng-class="{active: $uiRoute}" ui-route="/settings/log" href="#/settings/log">Server logs</a>
|
||||
</ul>
|
||||
|
@ -37,6 +37,15 @@
|
||||
<span class="help-block" ng-show="editUserForm.email.$error.maxlength">Too long</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="user.groups.length > 0">
|
||||
<label class="col-sm-2 control-label">Groups</label>
|
||||
|
||||
<div class="col-sm-7">
|
||||
<a class="btn btn-default"
|
||||
ng-repeat="group in user.groups"
|
||||
href="#/settings/group/edit/{{ group }}">{{ group }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !editUserForm.storage_quota.$valid, success: editUserForm.storage_quota.$valid }">
|
||||
<label class="col-sm-2 control-label" for="inputQuota">Storage quota</label>
|
||||
|
||||
@ -90,5 +99,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{ alert.msg }}</alert>
|
||||
</div>
|
@ -10,7 +10,8 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users | orderBy: 'username'" ng-click="editUser(user)">
|
||||
<tr ng-repeat="user in users | orderBy: 'username'" ng-click="editUser(user)"
|
||||
ng-class="{ active: $stateParams.username == user.username }">
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.create_date | date: 'yyyy-MM-dd' }}</td>
|
||||
</tr>
|
||||
|
@ -2,6 +2,13 @@
|
||||
<h1>{{ user.username }} <small>{{ user.email }}</small></h1>
|
||||
</div>
|
||||
|
||||
<h4 ng-if="user.groups.length > 0">Groups</h4>
|
||||
<ul ng-if="user.groups.length > 0">
|
||||
<li ng-repeat="group in user.groups">
|
||||
<a href="#/group/{{ group }}">{{ group }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Quota used</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
@ -71,6 +71,12 @@
|
||||
.table-users {
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
td {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,8 +108,31 @@ public class TestGroupResource extends BaseJerseyTest {
|
||||
groups = json.getJsonArray("groups");
|
||||
Assert.assertEquals(4, groups.size());
|
||||
|
||||
// Remove group1 from g12
|
||||
json = target().path("/group/g12/group1").request()
|
||||
// Update group g12
|
||||
target().path("/group/g12").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.post(Entity.form(new Form()
|
||||
.param("name", "g12new")
|
||||
.param("parent", "g11")), JsonObject.class);
|
||||
|
||||
// Check group1 groups with admin (only direct groups)
|
||||
json = target().path("/user/group1").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.get(JsonObject.class);
|
||||
groups = json.getJsonArray("groups");
|
||||
Assert.assertEquals(2, groups.size());
|
||||
Assert.assertEquals("g112", groups.getString(0));
|
||||
Assert.assertEquals("g12new", groups.getString(1));
|
||||
|
||||
// Get group g12new
|
||||
json = target().path("/group/g12new").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.get(JsonObject.class);
|
||||
Assert.assertEquals("g12new", json.getString("name"));
|
||||
Assert.assertEquals("g11", json.getString("parent"));
|
||||
|
||||
// Remove group1 from g12new
|
||||
json = target().path("/group/g12new/group1").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.delete(JsonObject.class);
|
||||
|
||||
@ -126,5 +149,23 @@ public class TestGroupResource extends BaseJerseyTest {
|
||||
Assert.assertTrue(groupList.contains("g1"));
|
||||
Assert.assertTrue(groupList.contains("g11"));
|
||||
Assert.assertTrue(groupList.contains("g112"));
|
||||
|
||||
// Delete group g1
|
||||
json = target().path("/group/g1").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
|
||||
.delete(JsonObject.class);
|
||||
|
||||
// Check group1 groups (all computed groups)
|
||||
json = target().path("/user").request()
|
||||
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, group1Token)
|
||||
.get(JsonObject.class);
|
||||
groups = json.getJsonArray("groups");
|
||||
groupList = new ArrayList<>();
|
||||
for (int i = 0; i < groups.size(); i++) {
|
||||
groupList.add(groups.getString(i));
|
||||
}
|
||||
Assert.assertEquals(2, groups.size());
|
||||
Assert.assertTrue(groupList.contains("g11"));
|
||||
Assert.assertTrue(groupList.contains("g112"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user