mirror of
https://github.com/google/nomulus.git
synced 2025-08-03 08:22:13 +02:00
Convert to tm() some low-hanging ofy fruit (#1029)
* Convert to tm() some low-hanging ofy fruit
This commit is contained in:
parent
2649a9362a
commit
1e650bd0a1
10 changed files with 118 additions and 60 deletions
|
@ -16,6 +16,7 @@ package google.registry.tools;
|
|||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
|
@ -61,13 +62,7 @@ final class DeleteTldCommand extends ConfirmingCommand implements CommandWithRem
|
|||
"Cannot delete TLD because registrar %s lists it as an allowed TLD",
|
||||
registrar.getClientId());
|
||||
}
|
||||
|
||||
int count = ofy().load()
|
||||
.type(DomainBase.class)
|
||||
.filter("tld", tld)
|
||||
.limit(1)
|
||||
.count();
|
||||
checkState(count == 0, "Cannot delete TLD because a domain is defined on it");
|
||||
checkState(!tldContainsDomains(tld), "Cannot delete TLD because a domain is defined on it");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -77,8 +72,25 @@ final class DeleteTldCommand extends ConfirmingCommand implements CommandWithRem
|
|||
|
||||
@Override
|
||||
protected String execute() {
|
||||
tm().transactNew(() -> ofy().delete().entity(registry).now());
|
||||
tm().transactNew(() -> tm().delete(registry));
|
||||
registry.invalidateInCache();
|
||||
return String.format("Deleted TLD '%s'.\n", tld);
|
||||
}
|
||||
|
||||
private boolean tldContainsDomains(String tld) {
|
||||
if (tm().isOfy()) {
|
||||
return ofy().load().type(DomainBase.class).filter("tld", tld).limit(1).count() > 0;
|
||||
} else {
|
||||
return jpaTm()
|
||||
.transact(
|
||||
() ->
|
||||
jpaTm()
|
||||
.query("FROM Domain WHERE tld = :tld", DomainBase.class)
|
||||
.setParameter("tld", tld)
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst()
|
||||
.isPresent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,17 +15,19 @@
|
|||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.base.Strings;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.model.common.Cursor;
|
||||
import google.registry.model.common.Cursor.CursorType;
|
||||
import google.registry.model.registry.Registries;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.TldType;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
|
@ -50,20 +52,18 @@ final class ListCursorsCommand implements CommandWithRemoteApi {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
Map<Registry, Key<Cursor>> registries =
|
||||
Registries.getTlds()
|
||||
.stream()
|
||||
Map<Registry, VKey<Cursor>> registries =
|
||||
Registries.getTlds().stream()
|
||||
.map(Registry::get)
|
||||
.filter(r -> r.getTldType() == filterTldType)
|
||||
.filter(r -> !filterEscrowEnabled || r.getEscrowEnabled())
|
||||
.collect(toImmutableMap(r -> r, r -> Cursor.createKey(cursorType, r)));
|
||||
Map<Key<Cursor>, Cursor> cursors = ofy().load().keys(registries.values());
|
||||
.collect(toImmutableMap(r -> r, r -> Cursor.createVKey(cursorType, r.getTldStr())));
|
||||
ImmutableMap<VKey<? extends Cursor>, Cursor> cursors =
|
||||
transactIfJpaTm(() -> tm().loadByKeysIfPresent(registries.values()));
|
||||
if (!registries.isEmpty()) {
|
||||
String header = String.format(OUTPUT_FMT, "TLD", "Cursor Time", "Last Update Time");
|
||||
System.out.printf("%s\n%s\n", header, Strings.repeat("-", header.length()));
|
||||
registries
|
||||
.entrySet()
|
||||
.stream()
|
||||
registries.entrySet().stream()
|
||||
.map(
|
||||
e ->
|
||||
renderLine(
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
package google.registry.tools.server;
|
||||
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static java.util.Comparator.comparing;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.model.EppResourceUtils;
|
||||
import google.registry.model.host.HostResource;
|
||||
import google.registry.request.Action;
|
||||
|
@ -51,7 +51,7 @@ public final class ListHostsAction extends ListObjectsAction<HostResource> {
|
|||
@Override
|
||||
public ImmutableSet<HostResource> loadObjects() {
|
||||
final DateTime now = clock.nowUtc();
|
||||
return Streams.stream(ofy().load().type(HostResource.class))
|
||||
return transactIfJpaTm(() -> tm().loadAllOf(HostResource.class)).stream()
|
||||
.filter(host -> EppResourceUtils.isActive(host, now))
|
||||
.collect(toImmutableSortedSet(comparing(HostResource::getHostName)));
|
||||
}
|
||||
|
|
|
@ -14,16 +14,21 @@
|
|||
|
||||
package google.registry.tools.server;
|
||||
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.registry.label.PremiumList;
|
||||
import google.registry.model.registry.label.PremiumListDualDao;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.auth.Auth;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
/**
|
||||
* An action that lists premium lists, for use by the {@code nomulus list_premium_lists} command.
|
||||
|
@ -46,7 +51,14 @@ public final class ListPremiumListsAction extends ListObjectsAction<PremiumList>
|
|||
|
||||
@Override
|
||||
public ImmutableSet<PremiumList> loadObjects() {
|
||||
return ImmutableSet.copyOf(
|
||||
ofy().load().type(PremiumList.class).ancestor(getCrossTldKey()).list());
|
||||
return transactIfJpaTm(
|
||||
() ->
|
||||
tm().loadAllOf(PremiumList.class).stream()
|
||||
.map(PremiumList::getName)
|
||||
.map(PremiumListDualDao::getLatestRevision)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.peek(list -> Hibernate.initialize(list.getLabelsToPrices()))
|
||||
.collect(toImmutableSortedSet(Comparator.comparing(PremiumList::getName))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,19 @@
|
|||
|
||||
package google.registry.tools.server;
|
||||
|
||||
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.persistence.transaction.TransactionManagerUtil.transactIfJpaTm;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.registry.label.ReservedList;
|
||||
import google.registry.model.registry.label.ReservedListDualDatabaseDao;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.auth.Auth;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** A that lists reserved lists, for use by the {@code nomulus list_reserved_lists} command. */
|
||||
|
@ -44,7 +48,13 @@ public final class ListReservedListsAction extends ListObjectsAction<ReservedLis
|
|||
|
||||
@Override
|
||||
public ImmutableSet<ReservedList> loadObjects() {
|
||||
return ImmutableSet.copyOf(
|
||||
ofy().load().type(ReservedList.class).ancestor(getCrossTldKey()).list());
|
||||
return transactIfJpaTm(
|
||||
() ->
|
||||
tm().loadAllOf(ReservedList.class).stream()
|
||||
.map(ReservedList::getName)
|
||||
.map(ReservedListDualDatabaseDao::getLatestRevision)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(toImmutableSortedSet(Comparator.comparing(ReservedList::getName))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,13 @@ import com.google.common.collect.ImmutableSortedMap;
|
|||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.Registry.RegistryNotFoundException;
|
||||
import google.registry.model.registry.Registry.TldType;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link DeleteTldCommand}. */
|
||||
@DualDatabaseTest
|
||||
class DeleteTldCommandTest extends CommandTestCase<DeleteTldCommand> {
|
||||
|
||||
private static final String TLD_REAL = "tldreal";
|
||||
|
@ -53,7 +55,7 @@ class DeleteTldCommandTest extends CommandTestCase<DeleteTldCommand> {
|
|||
TldType.TEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testSuccess_otherTldUnaffected() throws Exception {
|
||||
runCommandForced("--tld=" + TLD_TEST);
|
||||
|
||||
|
@ -61,24 +63,24 @@ class DeleteTldCommandTest extends CommandTestCase<DeleteTldCommand> {
|
|||
assertThrows(RegistryNotFoundException.class, () -> Registry.get(TLD_TEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_whenTldDoesNotExist() {
|
||||
assertThrows(RegistryNotFoundException.class, () -> runCommandForced("--tld=nonexistenttld"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_whenTldIsReal() {
|
||||
assertThrows(IllegalStateException.class, () -> runCommandForced("--tld=" + TLD_REAL));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_whenDomainsArePresent() {
|
||||
persistDeletedDomain("domain." + TLD_TEST, DateTime.parse("2000-01-01TZ"));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> runCommandForced("--tld=" + TLD_TEST));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testFailure_whenRegistrarLinksToTld() {
|
||||
allowRegistrarAccess("TheRegistrar", TLD_TEST);
|
||||
|
||||
|
|
|
@ -24,14 +24,15 @@ import google.registry.model.common.Cursor;
|
|||
import google.registry.model.common.Cursor.CursorType;
|
||||
import google.registry.model.ofy.Ofy;
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.InjectExtension;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
/** Unit tests for {@link ListCursorsCommand}. */
|
||||
@DualDatabaseTest
|
||||
public class ListCursorsCommandTest extends CommandTestCase<ListCursorsCommand> {
|
||||
|
||||
private static final String HEADER_ONE =
|
||||
|
@ -44,17 +45,17 @@ public class ListCursorsCommandTest extends CommandTestCase<ListCursorsCommand>
|
|||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
inject.setStaticField(
|
||||
Ofy.class, "clock", new FakeClock(DateTime.parse("1984-12-21T06:07:08.789Z")));
|
||||
fakeClock.setTo(DateTime.parse("1984-12-21T06:07:08.789Z"));
|
||||
inject.setStaticField(Ofy.class, "clock", fakeClock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testListCursors_noTlds_printsNothing() throws Exception {
|
||||
runCommand("--type=BRDA");
|
||||
assertThat(getStdoutAsString()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testListCursors_twoTldsOneAbsent_printsAbsentAndTimestampSorted() throws Exception {
|
||||
createTlds("foo", "bar");
|
||||
persistResource(
|
||||
|
@ -69,19 +70,19 @@ public class ListCursorsCommandTest extends CommandTestCase<ListCursorsCommand>
|
|||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testListCursors_badCursor_throwsIae() {
|
||||
ParameterException thrown =
|
||||
assertThrows(ParameterException.class, () -> runCommand("--type=love"));
|
||||
assertThat(thrown).hasMessageThat().contains("Invalid value for --type parameter.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testListCursors_lowercaseCursor_isAllowed() throws Exception {
|
||||
runCommand("--type=brda");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testListCursors_filterEscrowEnabled_doesWhatItSays() throws Exception {
|
||||
createTlds("foo", "bar");
|
||||
persistResource(Registry.get("bar").asBuilder().setEscrowEnabled(true).build());
|
||||
|
|
|
@ -17,13 +17,15 @@ package google.registry.tools.server;
|
|||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveHost;
|
||||
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ListHostsAction}. */
|
||||
@DualDatabaseTest
|
||||
class ListHostsActionTest extends ListActionTestCase {
|
||||
|
||||
private ListHostsAction action;
|
||||
|
@ -35,7 +37,7 @@ class ListHostsActionTest extends ListActionTestCase {
|
|||
action.clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_noParameters() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -44,7 +46,7 @@ class ListHostsActionTest extends ListActionTestCase {
|
|||
null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_twoLinesWithRepoId() {
|
||||
persistActiveHost("example2.foo");
|
||||
persistActiveHost("example1.foo");
|
||||
|
@ -59,7 +61,7 @@ class ListHostsActionTest extends ListActionTestCase {
|
|||
"^example2.foo\\s+2-ROID\\s*$");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_twoLinesWithWildcard() {
|
||||
persistActiveHost("example2.foo");
|
||||
persistActiveHost("example1.foo");
|
||||
|
@ -74,7 +76,7 @@ class ListHostsActionTest extends ListActionTestCase {
|
|||
"^example2.foo\\s+.*1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_twoLinesWithWildcardAndAnotherField() {
|
||||
persistActiveHost("example2.foo");
|
||||
persistActiveHost("example1.foo");
|
||||
|
@ -89,7 +91,7 @@ class ListHostsActionTest extends ListActionTestCase {
|
|||
"^example2.foo\\s+.*1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_withBadField_returnsError() {
|
||||
persistActiveHost("example2.foo");
|
||||
persistActiveHost("example1.foo");
|
||||
|
|
|
@ -16,11 +16,15 @@ package google.registry.tools.server;
|
|||
|
||||
import static google.registry.testing.DatabaseHelper.persistPremiumList;
|
||||
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import google.registry.testing.TestOfyOnly;
|
||||
import google.registry.testing.TestSqlOnly;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ListPremiumListsAction}. */
|
||||
@DualDatabaseTest
|
||||
class ListPremiumListsActionTest extends ListActionTestCase {
|
||||
|
||||
private ListPremiumListsAction action;
|
||||
|
@ -32,7 +36,7 @@ class ListPremiumListsActionTest extends ListActionTestCase {
|
|||
action = new ListPremiumListsAction();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_noParameters() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -43,7 +47,7 @@ class ListPremiumListsActionTest extends ListActionTestCase {
|
|||
"^xn--q9jyb4c$");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyOnly // only ofy has revisionKey
|
||||
void testRun_withParameters() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -56,7 +60,20 @@ class ListPremiumListsActionTest extends ListActionTestCase {
|
|||
"^xn--q9jyb4c\\s+.*PremiumList.*$");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSqlOnly
|
||||
void testRun_withLabelsToPrices() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
Optional.of("labelsToPrices"),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
"^name\\s+labelsToPrices\\s*$",
|
||||
"^-+\\s+-+\\s*$",
|
||||
"^how\\s+\\{richer=5000\\.00\\}$",
|
||||
"^xn--q9jyb4c\\s+\\{rich=100\\.00\\}\\s+$");
|
||||
}
|
||||
|
||||
@TestOfyOnly
|
||||
void testRun_withWildcard() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -69,7 +86,7 @@ class ListPremiumListsActionTest extends ListActionTestCase {
|
|||
"^xn--q9jyb4c\\s+.*PremiumList");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_withBadField_returnsError() {
|
||||
testRunError(
|
||||
action,
|
||||
|
|
|
@ -20,11 +20,13 @@ import static google.registry.testing.DatabaseHelper.persistResource;
|
|||
|
||||
import google.registry.model.registry.Registry;
|
||||
import google.registry.model.registry.label.ReservedList;
|
||||
import google.registry.testing.DualDatabaseTest;
|
||||
import google.registry.testing.TestOfyAndSql;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link ListReservedListsAction}. */
|
||||
@DualDatabaseTest
|
||||
class ListReservedListsActionTest extends ListActionTestCase {
|
||||
|
||||
private ListReservedListsAction action;
|
||||
|
@ -38,7 +40,7 @@ class ListReservedListsActionTest extends ListActionTestCase {
|
|||
action = new ListReservedListsAction();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_noParameters() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -49,7 +51,7 @@ class ListReservedListsActionTest extends ListActionTestCase {
|
|||
"^xn--q9jyb4c-published\\s*$");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_withParameters() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -62,7 +64,7 @@ class ListReservedListsActionTest extends ListActionTestCase {
|
|||
"^xn--q9jyb4c-published\\s+true\\s*$");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_withWildcard() {
|
||||
testRunSuccess(
|
||||
action,
|
||||
|
@ -75,7 +77,7 @@ class ListReservedListsActionTest extends ListActionTestCase {
|
|||
"^xn--q9jyb4c-published\\s+.*true");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestOfyAndSql
|
||||
void testRun_withBadField_returnsError() {
|
||||
testRunError(
|
||||
action,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue