Fixed null nextLinkName issue. Fixed paging tests to cover proper scenarios. Fixed null nextLinkName test.
This commit is contained in:
Родитель
7a2f6d5d48
Коммит
273870fa14
|
@ -244,7 +244,6 @@ namespace AutoRest.CSharp.V3.Generation.Writers
|
|||
var pageType = pagingMethod.ItemType;
|
||||
CSharpType responseType = async ? new CSharpType(typeof(AsyncPageable<>), pageType) : new CSharpType(typeof(Pageable<>), pageType);
|
||||
var parameters = pagingMethod.Method.Parameters;
|
||||
var nextPageParameters = pagingMethod.NextPageMethod.Parameters;
|
||||
|
||||
writer.WriteXmlDocumentationSummary(pagingMethod.Method.Description);
|
||||
|
||||
|
@ -286,17 +285,23 @@ namespace AutoRest.CSharp.V3.Generation.Writers
|
|||
writer.Line($"return {typeof(Page)}.FromValues(response.Value.{pagingMethod.ItemName}, {continuationTokenText}, response.GetRawResponse());");
|
||||
}
|
||||
|
||||
using (writer.Scope($"{asyncText} {funcType} NextPageFunc({typeof(string)} nextLink, {nullableInt} pageSizeHint)"))
|
||||
var nextPageFunctionName = "null";
|
||||
if (pagingMethod.NextPageMethod != null)
|
||||
{
|
||||
writer.Append($"var response = {awaitText} RestClient.{CreateMethodName(pagingMethod.NextPageMethod.Name, async)}(");
|
||||
foreach (Parameter parameter in nextPageParameters)
|
||||
nextPageFunctionName = "NextPageFunc";
|
||||
var nextPageParameters = pagingMethod.NextPageMethod.Parameters;
|
||||
using (writer.Scope($"{asyncText} {funcType} {nextPageFunctionName}({typeof(string)} nextLink, {nullableInt} pageSizeHint)"))
|
||||
{
|
||||
writer.Append($"{parameter.Name}, ");
|
||||
writer.Append($"var response = {awaitText} RestClient.{CreateMethodName(pagingMethod.NextPageMethod.Name, async)}(");
|
||||
foreach (Parameter parameter in nextPageParameters)
|
||||
{
|
||||
writer.Append($"{parameter.Name}, ");
|
||||
}
|
||||
writer.Line($"cancellationToken){configureAwaitText};");
|
||||
writer.Line($"return {typeof(Page)}.FromValues(response.Value.{pagingMethod.ItemName}, {continuationTokenText}, response.GetRawResponse());");
|
||||
}
|
||||
writer.Line($"cancellationToken){configureAwaitText};");
|
||||
writer.Line($"return {typeof(Page)}.FromValues(response.Value.{pagingMethod.ItemName}, {continuationTokenText}, response.GetRawResponse());");
|
||||
}
|
||||
writer.Line($"return {typeof(PageableHelpers)}.Create{(async ? "Async" : string.Empty)}Enumerable(FirstPageFunc, NextPageFunc);");
|
||||
writer.Line($"return {typeof(PageableHelpers)}.Create{(async ? "Async" : string.Empty)}Enumerable(FirstPageFunc, {nextPageFunctionName});");
|
||||
}
|
||||
writer.Line();
|
||||
}
|
||||
|
|
|
@ -106,10 +106,11 @@ namespace AutoRest.CSharp.V3.Output.Builders
|
|||
next = nextOperationMethod.Method;
|
||||
}
|
||||
// If there is no operationName or we didn't find an existing operation, we use the original method to construct the nextPageMethod.
|
||||
RestClientMethod nextPageMethod = next ?? BuildNextPageMethod(method);
|
||||
// Only add the method if it didn't previously exist
|
||||
if (next == null)
|
||||
RestClientMethod? nextPageMethod = next;
|
||||
// Only create and add a method if it didn't previously exist and we have a NextLinkName
|
||||
if (next == null && paging.NextLinkName != null)
|
||||
{
|
||||
nextPageMethod = BuildNextPageMethod(method);
|
||||
nextPageMethods.Add(nextPageMethod);
|
||||
}
|
||||
|
||||
|
@ -465,7 +466,7 @@ namespace AutoRest.CSharp.V3.Output.Builders
|
|||
method.Diagnostics);
|
||||
}
|
||||
|
||||
private PagingInfo GetPagingInfo(RestClientMethod method, RestClientMethod nextPageMethod, Paging paging, ObjectType type)
|
||||
private PagingInfo GetPagingInfo(RestClientMethod method, RestClientMethod? nextPageMethod, Paging paging, ObjectType type)
|
||||
{
|
||||
string? nextLinkName = paging.NextLinkName;
|
||||
string itemName = paging.ItemName ?? "value";
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace AutoRest.CSharp.V3.Output.Models.Requests
|
|||
{
|
||||
internal class PagingInfo
|
||||
{
|
||||
public PagingInfo(RestClientMethod method, RestClientMethod nextPageMethod, string name, string? nextLinkName, string itemName, CSharpType itemType)
|
||||
public PagingInfo(RestClientMethod method, RestClientMethod? nextPageMethod, string name, string? nextLinkName, string itemName, CSharpType itemType)
|
||||
{
|
||||
Method = method;
|
||||
NextPageMethod = nextPageMethod;
|
||||
|
@ -20,7 +20,7 @@ namespace AutoRest.CSharp.V3.Output.Models.Requests
|
|||
|
||||
public string Name { get; }
|
||||
public RestClientMethod Method { get; }
|
||||
public RestClientMethod NextPageMethod { get; }
|
||||
public RestClientMethod? NextPageMethod { get; }
|
||||
public string? NextLinkName { get; }
|
||||
public string ItemName { get; }
|
||||
public CSharpType ItemType { get; }
|
||||
|
|
|
@ -11,14 +11,18 @@ namespace Azure.Core
|
|||
{
|
||||
internal static class PageableHelpers
|
||||
{
|
||||
public static Pageable<T> CreateEnumerable<T>(Func<int?, Page<T>> firstPageFunc, Func<string?, int?, Page<T>> nextPageFunc, int? pageSize = default) where T : notnull
|
||||
public static Pageable<T> CreateEnumerable<T>(Func<int?, Page<T>> firstPageFunc, Func<string?, int?, Page<T>>? nextPageFunc, int? pageSize = default) where T : notnull
|
||||
{
|
||||
return new FuncPageable<T>((continuationToken, pageSizeHint) => firstPageFunc(pageSizeHint), (continuationToken, pageSizeHint) => nextPageFunc(continuationToken, pageSizeHint), pageSize);
|
||||
PageFunc<T> first = (continuationToken, pageSizeHint) => firstPageFunc(pageSizeHint);
|
||||
PageFunc<T>? next = nextPageFunc != null ? new PageFunc<T>(nextPageFunc) : null;
|
||||
return new FuncPageable<T>(first, next, pageSize);
|
||||
}
|
||||
|
||||
public static AsyncPageable<T> CreateAsyncEnumerable<T>(Func<int?, Task<Page<T>>> firstPageFunc, Func<string?, int?, Task<Page<T>>> nextPageFunc, int? pageSize = default) where T : notnull
|
||||
public static AsyncPageable<T> CreateAsyncEnumerable<T>(Func<int?, Task<Page<T>>> firstPageFunc, Func<string?, int?, Task<Page<T>>>? nextPageFunc, int? pageSize = default) where T : notnull
|
||||
{
|
||||
return new FuncAsyncPageable<T>((continuationToken, pageSizeHint) => firstPageFunc(pageSizeHint), (continuationToken, pageSizeHint) => nextPageFunc(continuationToken, pageSizeHint), pageSize);
|
||||
AsyncPageFunc<T> first = (continuationToken, pageSizeHint) => firstPageFunc(pageSizeHint);
|
||||
AsyncPageFunc<T>? next = nextPageFunc != null ? new AsyncPageFunc<T>(nextPageFunc) : null;
|
||||
return new FuncAsyncPageable<T>(first, next, pageSize);
|
||||
}
|
||||
|
||||
internal delegate Task<Page<T>> AsyncPageFunc<T>(string? continuationToken = default, int? pageSizeHint = default);
|
||||
|
@ -27,10 +31,10 @@ namespace Azure.Core
|
|||
internal class FuncAsyncPageable<T> : AsyncPageable<T> where T : notnull
|
||||
{
|
||||
private readonly AsyncPageFunc<T> _firstPageFunc;
|
||||
private readonly AsyncPageFunc<T> _nextPageFunc;
|
||||
private readonly AsyncPageFunc<T>? _nextPageFunc;
|
||||
private readonly int? _defaultPageSize;
|
||||
|
||||
public FuncAsyncPageable(AsyncPageFunc<T> firstPageFunc, AsyncPageFunc<T> nextPageFunc, int? defaultPageSize = default)
|
||||
public FuncAsyncPageable(AsyncPageFunc<T> firstPageFunc, AsyncPageFunc<T>? nextPageFunc, int? defaultPageSize = default)
|
||||
{
|
||||
_firstPageFunc = firstPageFunc;
|
||||
_nextPageFunc = nextPageFunc;
|
||||
|
@ -39,7 +43,7 @@ namespace Azure.Core
|
|||
|
||||
public override async IAsyncEnumerable<Page<T>> AsPages(string? continuationToken = default, int? pageSizeHint = default)
|
||||
{
|
||||
AsyncPageFunc<T> pageFunc = _firstPageFunc;
|
||||
AsyncPageFunc<T>? pageFunc = _firstPageFunc;
|
||||
int? pageSize = pageSizeHint ?? _defaultPageSize;
|
||||
do
|
||||
{
|
||||
|
@ -47,17 +51,17 @@ namespace Azure.Core
|
|||
yield return pageResponse;
|
||||
continuationToken = pageResponse.ContinuationToken;
|
||||
pageFunc = _nextPageFunc;
|
||||
} while (!string.IsNullOrEmpty(continuationToken));
|
||||
} while (!string.IsNullOrEmpty(continuationToken) && pageFunc != null);
|
||||
}
|
||||
}
|
||||
|
||||
internal class FuncPageable<T> : Pageable<T> where T : notnull
|
||||
{
|
||||
private readonly PageFunc<T> _firstPageFunc;
|
||||
private readonly PageFunc<T> _nextPageFunc;
|
||||
private readonly PageFunc<T>? _nextPageFunc;
|
||||
private readonly int? _defaultPageSize;
|
||||
|
||||
public FuncPageable(PageFunc<T> firstPageFunc, PageFunc<T> nextPageFunc, int? defaultPageSize = default)
|
||||
public FuncPageable(PageFunc<T> firstPageFunc, PageFunc<T>? nextPageFunc, int? defaultPageSize = default)
|
||||
{
|
||||
_firstPageFunc = firstPageFunc;
|
||||
_nextPageFunc = nextPageFunc;
|
||||
|
@ -66,7 +70,7 @@ namespace Azure.Core
|
|||
|
||||
public override IEnumerable<Page<T>> AsPages(string? continuationToken = default, int? pageSizeHint = default)
|
||||
{
|
||||
PageFunc<T> pageFunc = _firstPageFunc;
|
||||
PageFunc<T>? pageFunc = _firstPageFunc;
|
||||
int? pageSize = pageSizeHint ?? _defaultPageSize;
|
||||
do
|
||||
{
|
||||
|
@ -74,7 +78,7 @@ namespace Azure.Core
|
|||
yield return pageResponse;
|
||||
continuationToken = pageResponse.ContinuationToken;
|
||||
pageFunc = _nextPageFunc;
|
||||
} while (!string.IsNullOrEmpty(continuationToken));
|
||||
} while (!string.IsNullOrEmpty(continuationToken) && pageFunc != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ namespace AutoRest.TestServer.Tests
|
|||
}
|
||||
}
|
||||
Assert.AreEqual(2, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Request not matched.")]
|
||||
|
@ -134,7 +134,7 @@ namespace AutoRest.TestServer.Tests
|
|||
}
|
||||
}
|
||||
Assert.AreEqual(2, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Request not matched.")]
|
||||
|
@ -193,7 +193,7 @@ namespace AutoRest.TestServer.Tests
|
|||
product = "product";
|
||||
}
|
||||
Assert.AreEqual(10, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Refused connection.")]
|
||||
|
@ -254,7 +254,7 @@ namespace AutoRest.TestServer.Tests
|
|||
product = "product";
|
||||
}
|
||||
Assert.AreEqual(10, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Refused connection.")]
|
||||
|
@ -313,7 +313,7 @@ namespace AutoRest.TestServer.Tests
|
|||
}
|
||||
}
|
||||
Assert.AreEqual(2, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task PagingMultipleFailure() => Test(async (host, pipeline) =>
|
||||
|
@ -355,7 +355,7 @@ namespace AutoRest.TestServer.Tests
|
|||
}
|
||||
});
|
||||
Assert.AreEqual(2, id);
|
||||
}, ignoreScenario: true, useSimplePipeline: true);
|
||||
}, useSimplePipeline: true);
|
||||
|
||||
[Test]
|
||||
public Task PagingMultipleFailureUri() => Test(async (host, pipeline) =>
|
||||
|
@ -396,7 +396,7 @@ namespace AutoRest.TestServer.Tests
|
|||
}
|
||||
});
|
||||
Assert.AreEqual(2, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[Ignore("Needs LRO for paging: https://github.com/Azure/autorest.csharp/issues/450")]
|
||||
|
@ -483,7 +483,7 @@ namespace AutoRest.TestServer.Tests
|
|||
id++;
|
||||
}
|
||||
Assert.AreEqual(10, pageNumber);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Retry times out.")]
|
||||
|
@ -544,7 +544,7 @@ namespace AutoRest.TestServer.Tests
|
|||
product = "product";
|
||||
}
|
||||
Assert.AreEqual(10, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Retry times out.")]
|
||||
|
@ -611,7 +611,7 @@ namespace AutoRest.TestServer.Tests
|
|||
product = "product";
|
||||
}
|
||||
Assert.AreEqual(10, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Request not matched.")]
|
||||
|
@ -623,7 +623,7 @@ namespace AutoRest.TestServer.Tests
|
|||
Assert.AreEqual("Product", resultPage.Values.First().Properties.Name);
|
||||
Assert.IsNull(resultPage.ContinuationToken);
|
||||
|
||||
var pageableAsync = new PagingClient(ClientDiagnostics, pipeline, host).GetNoItemNamePagesAsync();
|
||||
var pageableAsync = new PagingClient(ClientDiagnostics, pipeline, host).GetNullNextLinkNamePagesAsync();
|
||||
await foreach (var page in pageableAsync.AsPages())
|
||||
{
|
||||
Assert.AreEqual(1, page.Values.First().Properties.Id);
|
||||
|
@ -631,14 +631,14 @@ namespace AutoRest.TestServer.Tests
|
|||
Assert.IsNull(page.ContinuationToken);
|
||||
}
|
||||
|
||||
var pageable = new PagingClient(ClientDiagnostics, pipeline, host).GetNoItemNamePages();
|
||||
var pageable = new PagingClient(ClientDiagnostics, pipeline, host).GetNullNextLinkNamePages();
|
||||
foreach (var page in pageable.AsPages())
|
||||
{
|
||||
Assert.AreEqual(1, page.Values.First().Properties.Id);
|
||||
Assert.AreEqual("Product", page.Values.First().Properties.Name);
|
||||
Assert.IsNull(page.ContinuationToken);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Request not matched.")]
|
||||
|
@ -665,7 +665,7 @@ namespace AutoRest.TestServer.Tests
|
|||
Assert.AreEqual("Product", page.Values.First().Properties.Name);
|
||||
Assert.IsNull(page.ContinuationToken);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
[IgnoreOnTestServer(TestServerVersion.V2, "Refused connection.")]
|
||||
|
@ -726,7 +726,7 @@ namespace AutoRest.TestServer.Tests
|
|||
product = "product";
|
||||
}
|
||||
Assert.AreEqual(10, id);
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task PagingSingle() => Test(async (host, pipeline) =>
|
||||
|
@ -752,7 +752,7 @@ namespace AutoRest.TestServer.Tests
|
|||
Assert.AreEqual("Product", page.Values.First().Properties.Name);
|
||||
Assert.IsNull(page.ContinuationToken);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task PagingSingleFailure() => Test((host, pipeline) =>
|
||||
|
@ -764,7 +764,6 @@ namespace AutoRest.TestServer.Tests
|
|||
|
||||
var pageable = new PagingClient(ClientDiagnostics, pipeline, host).GetSinglePagesFailure();
|
||||
Assert.Throws<RequestFailedException>(() => { foreach (var page in pageable.AsPages()) { } });
|
||||
}, true);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,12 +77,7 @@ namespace paging
|
|||
var response = await RestClient.GetNullNextLinkNamePagesAsync(cancellationToken).ConfigureAwait(false);
|
||||
return Page.FromValues(response.Value.Values, null, response.GetRawResponse());
|
||||
}
|
||||
async Task<Page<Product>> NextPageFunc(string nextLink, int? pageSizeHint)
|
||||
{
|
||||
var response = await RestClient.GetNullNextLinkNamePagesNextPageAsync(nextLink, cancellationToken).ConfigureAwait(false);
|
||||
return Page.FromValues(response.Value.Values, null, response.GetRawResponse());
|
||||
}
|
||||
return PageableHelpers.CreateAsyncEnumerable(FirstPageFunc, NextPageFunc);
|
||||
return PageableHelpers.CreateAsyncEnumerable(FirstPageFunc, null);
|
||||
}
|
||||
|
||||
/// <summary> A paging operation that must ignore any kind of nextLink, and stop after page 1. </summary>
|
||||
|
@ -94,12 +89,7 @@ namespace paging
|
|||
var response = RestClient.GetNullNextLinkNamePages(cancellationToken);
|
||||
return Page.FromValues(response.Value.Values, null, response.GetRawResponse());
|
||||
}
|
||||
Page<Product> NextPageFunc(string nextLink, int? pageSizeHint)
|
||||
{
|
||||
var response = RestClient.GetNullNextLinkNamePagesNextPage(nextLink, cancellationToken);
|
||||
return Page.FromValues(response.Value.Values, null, response.GetRawResponse());
|
||||
}
|
||||
return PageableHelpers.CreateEnumerable(FirstPageFunc, NextPageFunc);
|
||||
return PageableHelpers.CreateEnumerable(FirstPageFunc, null);
|
||||
}
|
||||
|
||||
/// <summary> A paging operation that finishes on the first call without a nextlink. </summary>
|
||||
|
|
|
@ -1837,104 +1837,6 @@ namespace paging
|
|||
}
|
||||
}
|
||||
|
||||
internal HttpMessage CreateGetNullNextLinkNamePagesNextPageRequest(string nextLink)
|
||||
{
|
||||
var message = _pipeline.CreateMessage();
|
||||
var request = message.Request;
|
||||
request.Method = RequestMethod.Get;
|
||||
var uri = new RawRequestUriBuilder();
|
||||
uri.AppendRaw(host, false);
|
||||
uri.AppendRawNextLink(nextLink, false);
|
||||
request.Uri = uri;
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary> A paging operation that must ignore any kind of nextLink, and stop after page 1. </summary>
|
||||
/// <param name="nextLink"> The URL to the next page of results. </param>
|
||||
/// <param name="cancellationToken"> The cancellation token to use. </param>
|
||||
public async ValueTask<Response<ProductResult>> GetNullNextLinkNamePagesNextPageAsync(string nextLink, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (nextLink == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nextLink));
|
||||
}
|
||||
|
||||
using var scope = _clientDiagnostics.CreateScope("PagingClient.GetNullNextLinkNamePages");
|
||||
scope.Start();
|
||||
try
|
||||
{
|
||||
using var message = CreateGetNullNextLinkNamePagesNextPageRequest(nextLink);
|
||||
await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
switch (message.Response.Status)
|
||||
{
|
||||
case 200:
|
||||
{
|
||||
ProductResult value = default;
|
||||
using var document = await JsonDocument.ParseAsync(message.Response.ContentStream, default, cancellationToken).ConfigureAwait(false);
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
value = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = ProductResult.DeserializeProductResult(document.RootElement);
|
||||
}
|
||||
return Response.FromValue(value, message.Response);
|
||||
}
|
||||
default:
|
||||
throw await _clientDiagnostics.CreateRequestFailedExceptionAsync(message.Response).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
scope.Failed(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> A paging operation that must ignore any kind of nextLink, and stop after page 1. </summary>
|
||||
/// <param name="nextLink"> The URL to the next page of results. </param>
|
||||
/// <param name="cancellationToken"> The cancellation token to use. </param>
|
||||
public Response<ProductResult> GetNullNextLinkNamePagesNextPage(string nextLink, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (nextLink == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nextLink));
|
||||
}
|
||||
|
||||
using var scope = _clientDiagnostics.CreateScope("PagingClient.GetNullNextLinkNamePages");
|
||||
scope.Start();
|
||||
try
|
||||
{
|
||||
using var message = CreateGetNullNextLinkNamePagesNextPageRequest(nextLink);
|
||||
_pipeline.Send(message, cancellationToken);
|
||||
switch (message.Response.Status)
|
||||
{
|
||||
case 200:
|
||||
{
|
||||
ProductResult value = default;
|
||||
using var document = JsonDocument.Parse(message.Response.ContentStream);
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
value = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = ProductResult.DeserializeProductResult(document.RootElement);
|
||||
}
|
||||
return Response.FromValue(value, message.Response);
|
||||
}
|
||||
default:
|
||||
throw _clientDiagnostics.CreateRequestFailedException(message.Response);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
scope.Failed(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
internal HttpMessage CreateGetSinglePagesNextPageRequest(string nextLink)
|
||||
{
|
||||
var message = _pipeline.CreateMessage();
|
||||
|
|
Загрузка…
Ссылка в новой задаче