Skip to content

Commit

Permalink
Add more filters for /voting endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Groxan committed Aug 21, 2024
1 parent ebb4c7c commit 56e6310
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 81 deletions.
20 changes: 12 additions & 8 deletions Tzkt.Api/Controllers/VotingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public Task<Proposal> GetProposalByHash([Required][ProtocolHash] string hash)
/// </remarks>
/// <param name="firstLevel">Filter by level of the first block of the period.</param>
/// <param name="lastLevel">Filter by level of the last block of the period.</param>
/// <param name="epoch">Filters by voting epoch</param>
/// <param name="select">Specify comma-separated list of fields to include into response or leave it undefined to return full object. If you select single field, response will be an array of values in both `.fields` and `.values` modes.</param>
/// <param name="sort">Sorts voting periods by specified field. Supported fields: `id` (default).</param>
/// <param name="offset">Specifies which or how many items should be skipped</param>
Expand All @@ -118,6 +119,7 @@ public Task<Proposal> GetProposalByHash([Required][ProtocolHash] string hash)
public async Task<ActionResult<IEnumerable<VotingPeriod>>> GetPeriods(
Int32Parameter firstLevel,
Int32Parameter lastLevel,
Int32Parameter epoch,
SelectParameter select,
SortParameter sort,
OffsetParameter offset,
Expand All @@ -129,25 +131,25 @@ public async Task<ActionResult<IEnumerable<VotingPeriod>>> GetPeriods(
#endregion

if (select == null)
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, sort, offset, limit));
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, epoch, sort, offset, limit));

if (select.Values != null)
{
if (select.Values.Length == 1)
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, sort, offset, limit, select.Values[0]));
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, epoch, sort, offset, limit, select.Values[0]));
else
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, sort, offset, limit, select.Values));
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, epoch, sort, offset, limit, select.Values));
}
else
{
if (select.Fields.Length == 1)
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, sort, offset, limit, select.Fields[0]));
return Ok(await Voting.GetPeriods(firstLevel, lastLevel, epoch, sort, offset, limit, select.Fields[0]));
else
{
return Ok(new SelectionResponse
{
Cols = select.Fields,
Rows = await Voting.GetPeriods(firstLevel, lastLevel, sort, offset, limit, select.Fields)
Rows = await Voting.GetPeriods(firstLevel, lastLevel, epoch, sort, offset, limit, select.Fields)
});
}
}
Expand Down Expand Up @@ -271,22 +273,24 @@ public Task<VoterSnapshot> GetPeriodVoter([Required][TzAddress] string address)
/// <remarks>
/// Returns a list of voting epochs.
/// </remarks>
/// <param name="sort">Sorts voting epochs by specified field. Supported fields: `id` (default).</param>
/// <param name="status">Filter by voting epoch status (`no_proposals`, `voting`, `completed`, `failed`).</param>
/// <param name="sort">Sorts voting epochs by specified field. Supported fields: `index` (default).</param>
/// <param name="offset">Specifies which or how many items should be skipped</param>
/// <param name="limit">Maximum number of items to return</param>
/// <returns></returns>
[HttpGet("epochs")]
public async Task<ActionResult<IEnumerable<VotingEpoch>>> GetEpochs(
EpochStatusParameter status,
SortParameter sort,
OffsetParameter offset,
[Range(0, 10000)] int limit = 100)
{
#region validate
if (sort != null && !sort.Validate("id"))
if (sort != null && !sort.Validate("id", "index"))
return new BadRequest($"{nameof(sort)}", "Sorting by the specified field is not allowed.");
#endregion

return Ok(await Voting.GetEpochs(sort, offset, limit));
return Ok(await Voting.GetEpochs(status, sort, offset, limit));
}

/// <summary>
Expand Down
60 changes: 60 additions & 0 deletions Tzkt.Api/Extensions/ModelBindingContextExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,66 @@ public static bool TryGetSrc1HashList(this ModelBindingContext bindingContext, s
return true;
}

public static bool TryGetEpochStatus(this ModelBindingContext bindingContext, string name, ref bool hasValue, out string result)
{
result = null;
var valueObject = bindingContext.ValueProvider.GetValue(name);

if (valueObject != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(name, valueObject);
if (!string.IsNullOrEmpty(valueObject.FirstValue))
{
if (!EpochStatuses.IsValid(valueObject.FirstValue))
{
bindingContext.ModelState.TryAddModelError(name, "Invalid epoch status.");
return false;
}
hasValue = true;
result = valueObject.FirstValue;
}
}

return true;
}

public static bool TryGetEpochStatusList(this ModelBindingContext bindingContext, string name, ref bool hasValue, out List<string> result)
{
result = null;
var valueObject = bindingContext.ValueProvider.GetValue(name);

if (valueObject != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(name, valueObject);
if (!string.IsNullOrEmpty(valueObject.FirstValue))
{
var rawValues = valueObject.FirstValue.Split(',', StringSplitOptions.RemoveEmptyEntries);

if (rawValues.Length == 0)
{
bindingContext.ModelState.TryAddModelError(name, "List should contain at least one item.");
return false;
}

hasValue = true;
result = new List<string>(rawValues.Length);

foreach (var rawValue in rawValues)
{
if (!EpochStatuses.IsValid(rawValue))
{
bindingContext.ModelState.TryAddModelError(name, "List contains invalid epoch status.");
return false;
}
hasValue = true;
result.Add(rawValue);
}
}
}

return true;
}

public static bool TryGetContractKind(this ModelBindingContext bindingContext, string name, ref bool hasValue, out int? result)
{
result = null;
Expand Down
44 changes: 44 additions & 0 deletions Tzkt.Api/Parameters/Binders/EpochStatusBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Tzkt.Api
{
public class EpochStatusBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var model = bindingContext.ModelName;
var hasValue = false;

if (!bindingContext.TryGetEpochStatus($"{model}", ref hasValue, out var value))
return Task.CompletedTask;

if (!bindingContext.TryGetEpochStatus($"{model}.eq", ref hasValue, out var eq))
return Task.CompletedTask;

if (!bindingContext.TryGetEpochStatus($"{model}.ne", ref hasValue, out var ne))
return Task.CompletedTask;

if (!bindingContext.TryGetEpochStatusList($"{model}.in", ref hasValue, out var @in))
return Task.CompletedTask;

if (!bindingContext.TryGetEpochStatusList($"{model}.ni", ref hasValue, out var ni))
return Task.CompletedTask;

if (!hasValue)
{
bindingContext.Result = ModelBindingResult.Success(null);
return Task.CompletedTask;
}

bindingContext.Result = ModelBindingResult.Success(new EpochStatusParameter
{
Eq = value ?? eq,
Ne = ne,
In = @in,
Ni = ni
});

return Task.CompletedTask;
}
}
}
71 changes: 71 additions & 0 deletions Tzkt.Api/Parameters/EpochStatusParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using NJsonSchema.Annotations;

namespace Tzkt.Api
{
[ModelBinder(BinderType = typeof(EpochStatusBinder))]
[JsonSchemaExtensionData("x-tzkt-extension", "query-parameter")]
[JsonSchemaExtensionData("x-tzkt-query-parameter", "no_proposals,voting,completed,failed")]
public class EpochStatusParameter : INormalizable
{
/// <summary>
/// **Equal** filter mode (optional, i.e. `param.eq=123` is the same as `param=123`). \
/// Specify an epoch status to get items where the specified field is equal to the specified value.
///
/// Example: `?status=completed`.
/// </summary>
public string Eq { get; set; }

/// <summary>
/// **Not equal** filter mode. \
/// Specify an epoch status to get items where the specified field is not equal to the specified value.
///
/// Example: `?status.ne=no_proposals`.
/// </summary>
public string Ne { get; set; }

/// <summary>
/// **In list** (any of) filter mode. \
/// Specify a comma-separated list of epoch statuses to get items where the specified field is equal to one of the specified values.
///
/// Example: `?status.in=completed,failed`.
/// </summary>
public List<string> In { get; set; }

/// <summary>
/// **Not in list** (none of) filter mode. \
/// Specify a comma-separated list of epoch statuses to get items where the specified field is not equal to all the specified values.
///
/// Example: `?status.ni=completed,failed`.
/// </summary>
public List<string> Ni { get; set; }

public string Normalize(string name)
{
var sb = new StringBuilder();

if (Eq != null)
{
sb.Append($"{name}.eq={Eq}&");
}

if (Ne != null)
{
sb.Append($"{name}.ne={Ne}&");
}

if (In?.Count > 0)
{
sb.Append($"{name}.in={string.Join(",", In.OrderBy(x => x))}&");
}

if (Ni?.Count > 0)
{
sb.Append($"{name}.ni={string.Join(",", Ni.OrderBy(x => x))}&");
}

return sb.ToString();
}
}
}
9 changes: 9 additions & 0 deletions Tzkt.Api/Repositories/Enums/EpochStatuses.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,14 @@ static class EpochStatuses
public const string Voting = "voting";
public const string Completed = "completed";
public const string Failed = "failed";

public static bool IsValid(string value) => value switch
{
NoProposals => true,
Voting => true,
Completed => true,
Failed => true,
_ => false
};
}
}
Loading

0 comments on commit 56e6310

Please sign in to comment.