Exploration des activités
This commit is contained in:
parent
2a8a4fc4a4
commit
7b92745153
20 changed files with 743 additions and 61 deletions
|
|
@ -1,5 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.0.8-rc10] - unstable
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
## [1.0.8-rc9] - unstable
|
## [1.0.8-rc9] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ public class ActivitiesPageViewModelTests
|
||||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||||
|
|
||||||
await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken);
|
await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken);
|
||||||
await client.GetPerformersAsync("brush-pro", TestContext.Current.CancellationToken);
|
await client.GetUsersAsync("brush-pro", TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]);
|
Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]);
|
||||||
Assert.Equal("https://business.example/api/v1/activity/brush-pro/performers", api.Paths[1]);
|
Assert.Equal("https://business.example/api/v1/activity/brush-pro/users", api.Paths[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -34,12 +34,20 @@ public class ActivitiesPageViewModelTests
|
||||||
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
Assert.Equal("brush", vm.CurrentActivity?.Code);
|
||||||
Assert.Single(vm.Performers);
|
Assert.Single(vm.Performers);
|
||||||
Assert.Equal("Alice", vm.Performers[0].UserName);
|
Assert.Equal("Alice", vm.Performers[0].UserName);
|
||||||
|
Assert.True(vm.Performers[0].HasPerformerProfile);
|
||||||
|
Assert.True(vm.Performers[0].IsPerformerActive);
|
||||||
|
Assert.Equal("Actif", vm.Performers[0].PerformerStatusBadgeLabel);
|
||||||
|
Assert.Equal("Pas d'autre activité", vm.Performers[0].ExtraActivityLabel);
|
||||||
|
|
||||||
await vm.ShowSpecializationAsync(vm.Specializations[0]);
|
await vm.ShowSpecializationAsync(vm.Specializations[0]);
|
||||||
|
|
||||||
Assert.Equal("brush-pro", vm.CurrentActivity?.Code);
|
Assert.Equal("brush-pro", vm.CurrentActivity?.Code);
|
||||||
Assert.Single(vm.Performers);
|
Assert.Single(vm.Performers);
|
||||||
Assert.Equal("Bob", vm.Performers[0].UserName);
|
Assert.Equal("Bob", vm.Performers[0].UserName);
|
||||||
|
Assert.True(vm.Performers[0].HasPerformerProfile);
|
||||||
|
Assert.False(vm.Performers[0].IsPerformerActive);
|
||||||
|
Assert.Equal("Inactif", vm.Performers[0].PerformerStatusBadgeLabel);
|
||||||
|
Assert.Equal("Autres spécialisations: 2", vm.Performers[0].ExtraActivityLabel);
|
||||||
Assert.Contains("brush pro", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("brush pro", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
await vm.ShowSpecializationAsync(null);
|
await vm.ShowSpecializationAsync(null);
|
||||||
|
|
@ -86,14 +94,14 @@ public class ActivitiesPageViewModelTests
|
||||||
|
|
||||||
if (typeof(T) == typeof(List<ActivityPerformerDto>))
|
if (typeof(T) == typeof(List<ActivityPerformerDto>))
|
||||||
{
|
{
|
||||||
var performers = path.EndsWith("brush-pro/performers", StringComparison.Ordinal)
|
var performers = path.EndsWith("brush-pro/users", StringComparison.Ordinal)
|
||||||
? new List<ActivityPerformerDto>
|
? new List<ActivityPerformerDto>
|
||||||
{
|
{
|
||||||
new() { PerformerId = "pro-2", UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro" }
|
new() { PerformerId = "pro-2", HasPerformerProfile = true, Active = false, UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro", ExtraActivityCount = 2 }
|
||||||
}
|
}
|
||||||
: new List<ActivityPerformerDto>
|
: new List<ActivityPerformerDto>
|
||||||
{
|
{
|
||||||
new() { PerformerId = "pro-1", UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush" }
|
new() { PerformerId = "pro-1", HasPerformerProfile = true, Active = true, UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush", ExtraActivityCount = 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.FromResult((T)(object)performers);
|
return Task.FromResult((T)(object)performers);
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,6 @@ private void ConfigureRootView(MainView rootView)
|
||||||
rootView.NavRoot.PopToRootAsync();
|
rootView.NavRoot.PopToRootAsync();
|
||||||
};
|
};
|
||||||
|
|
||||||
sessionStatus.LoginSucceeded += async () =>
|
|
||||||
{
|
|
||||||
await PushMainPageAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
rootView.SessionBanner.DataContext = sessionStatus;
|
rootView.SessionBanner.DataContext = sessionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; }
|
public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ObservableCollection<ActivityPerformerDto> Performers { get; set; } = new();
|
public partial ObservableCollection<ActivityUserDisplayItem> Performers { get; set; } = new();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsBusy { get; set; }
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
@ -122,14 +122,14 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
StatusMessage = $"Erreur: {ex.Message}";
|
StatusMessage = $"Erreur: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -158,7 +158,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
|
|
||||||
if (activity is null)
|
if (activity is null)
|
||||||
{
|
{
|
||||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,18 +197,19 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var list = await _client.GetPerformersAsync(activity.Code);
|
var list = await _client.GetUsersAsync(activity.Code);
|
||||||
Performers = new ObservableCollection<ActivityPerformerDto>(list ?? new());
|
Performers = new ObservableCollection<ActivityUserDisplayItem>((list ?? new())
|
||||||
StatusMessage = $"{activity.Name} · {Performers.Count} prestataire(s)";
|
.Select(ActivityUserDisplayItem.FromDto));
|
||||||
|
StatusMessage = $"{activity.Name} · {Performers.Count} utilisateur(s)";
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Performers = new ObservableCollection<ActivityPerformerDto>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
StatusMessage = $"Erreur: {ex.Message}";
|
StatusMessage = $"Erreur: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
39
src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs
Normal file
39
src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
using Yavsc.Abstract.Workflow;
|
||||||
|
|
||||||
|
namespace PostIt.ViewModels;
|
||||||
|
|
||||||
|
public sealed class ActivityUserDisplayItem
|
||||||
|
{
|
||||||
|
public string PerformerId { get; init; } = string.Empty;
|
||||||
|
public bool HasPerformerProfile { get; init; }
|
||||||
|
public string PerformerBadgeLabel { get; init; } = "Profil pro";
|
||||||
|
public bool IsPerformerActive { get; init; }
|
||||||
|
public string PerformerStatusBadgeLabel { get; init; } = "Inactif";
|
||||||
|
public string PerformerStatusBadgeBackground { get; init; } = "#FDECEA";
|
||||||
|
public string PerformerStatusBadgeBorder { get; init; } = "#C62828";
|
||||||
|
public string PerformerStatusBadgeForeground { get; init; } = "#8E0000";
|
||||||
|
public string UserName { get; init; } = string.Empty;
|
||||||
|
public string WebSite { get; init; } = string.Empty;
|
||||||
|
public int ExtraActivityCount { get; init; }
|
||||||
|
public string ExtraActivityLabel { get; init; } = "Pas d'autre activité";
|
||||||
|
|
||||||
|
public static ActivityUserDisplayItem FromDto(ActivityPerformerDto dto)
|
||||||
|
{
|
||||||
|
return new ActivityUserDisplayItem
|
||||||
|
{
|
||||||
|
PerformerId = dto.PerformerId,
|
||||||
|
HasPerformerProfile = dto.HasPerformerProfile,
|
||||||
|
UserName = dto.UserName,
|
||||||
|
WebSite = dto.WebSite,
|
||||||
|
IsPerformerActive = dto.Active,
|
||||||
|
PerformerStatusBadgeLabel = dto.Active ? "Actif" : "Inactif",
|
||||||
|
PerformerStatusBadgeBackground = dto.Active ? "#E6F7EC" : "#FDECEA",
|
||||||
|
PerformerStatusBadgeBorder = dto.Active ? "#2E7D32" : "#C62828",
|
||||||
|
PerformerStatusBadgeForeground = dto.Active ? "#1B5E20" : "#8E0000",
|
||||||
|
ExtraActivityCount = dto.ExtraActivityCount,
|
||||||
|
ExtraActivityLabel = dto.ExtraActivityCount == 0
|
||||||
|
? "Pas d'autre activité"
|
||||||
|
: $"Autres spécialisations: {dto.ExtraActivityCount}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,20 @@
|
||||||
x:DataType="vm:ActivitiesPageViewModel"
|
x:DataType="vm:ActivitiesPageViewModel"
|
||||||
Header="Activités">
|
Header="Activités">
|
||||||
<Grid RowDefinitions="Auto,*,Auto" Margin="12">
|
<Grid RowDefinitions="Auto,*,Auto" Margin="12">
|
||||||
|
<Grid.Styles>
|
||||||
|
<Style Selector="Border.user-badge">
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="Padding" Value="5,0" />
|
||||||
|
<Setter Property="MinHeight" Value="16" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.user-badge-text">
|
||||||
|
<Setter Property="FontSize" Value="9" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Styles>
|
||||||
|
|
||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8">
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8">
|
||||||
<Button Content="Rafraîchir" Command="{Binding RefreshCommand}" />
|
<Button Content="Rafraîchir" Command="{Binding RefreshCommand}" />
|
||||||
<TextBlock Text="Catalogue des activités" FontWeight="Bold" VerticalAlignment="Center" />
|
<TextBlock Text="Catalogue des activités" FontWeight="Bold" VerticalAlignment="Center" />
|
||||||
|
|
@ -22,7 +36,7 @@
|
||||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
||||||
<TextBlock Text="{Binding PerformerCount, StringFormat='Prestataires: {0}'}"
|
<TextBlock Text="{Binding PerformerCount, StringFormat='Utilisateurs: {0}'}"
|
||||||
FontSize="11" Opacity="0.6" />
|
FontSize="11" Opacity="0.6" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
@ -43,7 +57,7 @@
|
||||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
|
||||||
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
|
||||||
<TextBlock Text="{Binding PerformerCount, StringFormat='Prestataires: {0}'}"
|
<TextBlock Text="{Binding PerformerCount, StringFormat='Utilisateurs: {0}'}"
|
||||||
FontSize="11" Opacity="0.6" />
|
FontSize="11" Opacity="0.6" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
@ -52,17 +66,39 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="4" RowDefinitions="Auto,Auto,*">
|
<Grid Grid.Column="4" RowDefinitions="Auto,Auto,*">
|
||||||
<TextBlock Grid.Row="0" Text="Prestataires" FontWeight="Bold" Margin="0,0,0,8" />
|
<TextBlock Grid.Row="0" Text="Utilisateurs" FontWeight="Bold" Margin="0,0,0,8" />
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock Grid.Row="1"
|
||||||
Text="{Binding CurrentActivityLabel, StringFormat='Activité affichée : {0}'}"
|
Text="{Binding CurrentActivityLabel, StringFormat='Activité affichée : {0}'}"
|
||||||
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
||||||
<ListBox Grid.Row="2" ItemsSource="{Binding Performers}">
|
<ListBox Grid.Row="2" ItemsSource="{Binding Performers}">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="wf:ActivityPerformerDto">
|
<DataTemplate x:DataType="vm:ActivityUserDisplayItem">
|
||||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||||
<TextBlock Text="{Binding UserName}" FontWeight="Bold" />
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
|
<TextBlock Text="{Binding UserName}" FontWeight="Bold" />
|
||||||
|
<Border IsVisible="{Binding HasPerformerProfile}"
|
||||||
|
Classes="user-badge"
|
||||||
|
Background="#D9F5E5"
|
||||||
|
BorderBrush="#2E7D32"
|
||||||
|
>
|
||||||
|
<TextBlock Text="{Binding PerformerBadgeLabel}"
|
||||||
|
Classes="user-badge-text"
|
||||||
|
Foreground="#1B5E20"
|
||||||
|
/>
|
||||||
|
</Border>
|
||||||
|
<Border IsVisible="{Binding HasPerformerProfile}"
|
||||||
|
Classes="user-badge"
|
||||||
|
Background="{Binding PerformerStatusBadgeBackground}"
|
||||||
|
BorderBrush="{Binding PerformerStatusBadgeBorder}"
|
||||||
|
>
|
||||||
|
<TextBlock Text="{Binding PerformerStatusBadgeLabel}"
|
||||||
|
Classes="user-badge-text"
|
||||||
|
Foreground="{Binding PerformerStatusBadgeForeground}"
|
||||||
|
/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
<TextBlock Text="{Binding WebSite}" FontSize="11" Opacity="0.7" />
|
<TextBlock Text="{Binding WebSite}" FontSize="11" Opacity="0.7" />
|
||||||
<TextBlock Text="{Binding ExtraActivityCount, StringFormat='Autres spécialisations: {0}'}"
|
<TextBlock Text="{Binding ExtraActivityLabel}"
|
||||||
FontSize="11" Opacity="0.6" />
|
FontSize="11" Opacity="0.6" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Yavsc.Abstract.Workflow;
|
||||||
public sealed class ActivityPerformerDto
|
public sealed class ActivityPerformerDto
|
||||||
{
|
{
|
||||||
public string PerformerId { get; set; } = string.Empty;
|
public string PerformerId { get; set; } = string.Empty;
|
||||||
|
public bool HasPerformerProfile { get; set; }
|
||||||
public string UserName { get; set; } = string.Empty;
|
public string UserName { get; set; } = string.Empty;
|
||||||
public bool Active { get; set; }
|
public bool Active { get; set; }
|
||||||
public bool AcceptNotifications { get; set; }
|
public bool AcceptNotifications { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ public sealed class ActivityApiClient
|
||||||
return _api.CallAsync<List<ActivityBrowseItemDto>>(HttpMethod.Get, Absolute(path), ct: ct);
|
return _api.CallAsync<List<ActivityBrowseItemDto>>(HttpMethod.Get, Absolute(path), ct: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<ActivityPerformerDto>> GetPerformersAsync(
|
public Task<List<ActivityPerformerDto>> GetUsersAsync(
|
||||||
string activityCode,
|
string activityCode,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|
@ -48,9 +48,14 @@ public sealed class ActivityApiClient
|
||||||
|
|
||||||
return _api.CallAsync<List<ActivityPerformerDto>>(
|
return _api.CallAsync<List<ActivityPerformerDto>>(
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(activityCode)}/performers"),
|
Absolute($"{PathPrefix}/{Uri.EscapeDataString(activityCode)}/users"),
|
||||||
ct: ct);
|
ct: ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<List<ActivityPerformerDto>> GetPerformersAsync(
|
||||||
|
string activityCode,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
=> GetUsersAsync(activityCode, ct);
|
||||||
|
|
||||||
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
161
src/Yavsc.Api.Test/ActivityApiControllerTests.cs
Normal file
161
src/Yavsc.Api.Test/ActivityApiControllerTests.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Yavsc.Abstract.Workflow;
|
||||||
|
using Yavsc.Api.Test.Fixtures;
|
||||||
|
using Yavsc.Tests.Shared;
|
||||||
|
|
||||||
|
namespace Yavsc.Api.Test;
|
||||||
|
|
||||||
|
[Collection("Yavsc Api")]
|
||||||
|
public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixture>
|
||||||
|
{
|
||||||
|
private readonly ApiWebServerFixture _fixture;
|
||||||
|
|
||||||
|
public ActivityApiControllerTests(ApiWebServerFixture fixture)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient NewClient(string subject = "alice", string scope = "api")
|
||||||
|
{
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
||||||
|
};
|
||||||
|
var http = new HttpClient(handler)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(_fixture.BaseAddress)
|
||||||
|
};
|
||||||
|
http.DefaultRequestHeaders.Authorization =
|
||||||
|
new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
|
||||||
|
return http;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetUsers_returns_declared_user_for_exact_activity_code()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
using var http = NewClient();
|
||||||
|
|
||||||
|
var response = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(payload);
|
||||||
|
Assert.Single(payload!);
|
||||||
|
Assert.Equal("alice", payload[0].PerformerId);
|
||||||
|
Assert.Equal("alice", payload[0].UserName);
|
||||||
|
Assert.True(payload[0].HasPerformerProfile);
|
||||||
|
Assert.True(payload[0].Active);
|
||||||
|
Assert.Equal("dev", payload[0].ActivityCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetUsers_returns_user_even_when_performer_inactive()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
using (var scope = _fixture.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
|
||||||
|
var performer = db.Performers.Single(p => p.PerformerId == "alice");
|
||||||
|
performer.Active = false;
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var http = NewClient();
|
||||||
|
var response = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(payload);
|
||||||
|
Assert.Single(payload!);
|
||||||
|
Assert.Equal("alice", payload[0].PerformerId);
|
||||||
|
Assert.False(payload[0].Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Catalog_and_Performers_are_consistent_for_dev_activity()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
using var http = NewClient();
|
||||||
|
|
||||||
|
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
|
||||||
|
|
||||||
|
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(catalog);
|
||||||
|
|
||||||
|
var dev = catalog!.Single(a => a.Code == "dev");
|
||||||
|
Assert.True(dev.PerformerCount > 0);
|
||||||
|
|
||||||
|
var performersResponse = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, performersResponse.StatusCode);
|
||||||
|
|
||||||
|
var performers = await performersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(performers);
|
||||||
|
Assert.Equal(dev.PerformerCount, performers!.Count);
|
||||||
|
Assert.Contains(performers, p => p.PerformerId == "alice");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Catalog_does_not_list_activity_without_declaration()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
using var http = NewClient();
|
||||||
|
|
||||||
|
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
|
||||||
|
|
||||||
|
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(catalog);
|
||||||
|
Assert.DoesNotContain(catalog!, a => a.Code == "ghost");
|
||||||
|
Assert.Contains(catalog!, a => a.Code == "dev");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Catalog_lists_declared_activity_even_when_performer_inactive()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
using var http = NewClient();
|
||||||
|
|
||||||
|
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
|
||||||
|
|
||||||
|
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(catalog);
|
||||||
|
Assert.Contains(catalog!, a => a.Code == "declared-only");
|
||||||
|
|
||||||
|
var response = await http.GetAsync("/api/v1/activity/declared-only/users", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(payload);
|
||||||
|
Assert.Single(payload!);
|
||||||
|
Assert.Equal("bob", payload[0].PerformerId);
|
||||||
|
Assert.Equal("bob", payload[0].UserName);
|
||||||
|
Assert.True(payload[0].HasPerformerProfile);
|
||||||
|
Assert.False(payload[0].Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Performers_alias_returns_same_payload_as_users_endpoint()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
using var http = NewClient();
|
||||||
|
|
||||||
|
var usersResponse = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||||
|
var performersResponse = await http.GetAsync("/api/v1/activity/dev/performers", TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, usersResponse.StatusCode);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, performersResponse.StatusCode);
|
||||||
|
|
||||||
|
var usersPayload = await usersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||||
|
var performersPayload = await performersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.NotNull(usersPayload);
|
||||||
|
Assert.NotNull(performersPayload);
|
||||||
|
Assert.Equal(usersPayload!.Count, performersPayload!.Count);
|
||||||
|
Assert.Equal(usersPayload[0].PerformerId, performersPayload[0].PerformerId);
|
||||||
|
Assert.Equal(usersPayload[0].UserName, performersPayload[0].UserName);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/Yavsc.Api.Test/ApiCollection.cs
Normal file
6
src/Yavsc.Api.Test/ApiCollection.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Yavsc.Api.Test;
|
||||||
|
|
||||||
|
[CollectionDefinition("Yavsc Api")]
|
||||||
|
public sealed class ApiCollection
|
||||||
|
{
|
||||||
|
}
|
||||||
7
src/Yavsc.Api.Test/Directory.Packages.props
Normal file
7
src/Yavsc.Api.Test/Directory.Packages.props
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project>
|
||||||
|
<!--
|
||||||
|
Yavsc.Api.Test has no project-specific package versions. All
|
||||||
|
package versions are declared at the repository root.
|
||||||
|
-->
|
||||||
|
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Packages.props', '$(MSBuildThisFileDirectory)../'))" />
|
||||||
|
</Project>
|
||||||
210
src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
Normal file
210
src/Yavsc.Api.Test/Fixtures/ApiWebServerFixture.cs
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Yavsc.Controllers;
|
||||||
|
using Yavsc.Models;
|
||||||
|
using Yavsc.Models.Relationship;
|
||||||
|
using Yavsc.Models.Workflow;
|
||||||
|
using Yavsc.Tests.Shared;
|
||||||
|
|
||||||
|
namespace Yavsc.Api.Test.Fixtures;
|
||||||
|
|
||||||
|
public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
|
{
|
||||||
|
protected override int HttpsPort => 5104;
|
||||||
|
|
||||||
|
private static SqliteConnection? _sharedSqliteConnection;
|
||||||
|
private static readonly object _sqliteLock = new();
|
||||||
|
|
||||||
|
protected override WebApplication BuildApp(WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
SqliteConnection sharedConnection;
|
||||||
|
lock (_sqliteLock)
|
||||||
|
{
|
||||||
|
if (_sharedSqliteConnection is null)
|
||||||
|
{
|
||||||
|
_sharedSqliteConnection = new SqliteConnection(
|
||||||
|
"Data Source=YavscApiTests;Mode=Memory;Cache=Shared");
|
||||||
|
_sharedSqliteConnection.Open();
|
||||||
|
}
|
||||||
|
sharedConnection = _sharedSqliteConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<ApplicationDbContext>(opt =>
|
||||||
|
opt.UseSqlite(sharedConnection));
|
||||||
|
|
||||||
|
builder.Services.AddControllers()
|
||||||
|
.AddApplicationPart(typeof(ActivityApiController).Assembly);
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication("Bearer")
|
||||||
|
.AddJwtBearer("Bearer", options =>
|
||||||
|
{
|
||||||
|
options.IncludeErrorDetails = true;
|
||||||
|
options.MapInboundClaims = false;
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = TestTokenIssuer.Issuer,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = TestTokenIssuer.SigningKey,
|
||||||
|
NameClaimType = "sub",
|
||||||
|
RoleClaimType = Yavsc.Constants.RoleClaimType,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<WebApplication> ConfigurePipelineAsync(WebApplication app)
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BaseAddress => Addresses.First(a => a.StartsWith("https://", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
public void ResetAndSeedActivityGraph()
|
||||||
|
{
|
||||||
|
using var scope = Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
|
||||||
|
db.Database.EnsureDeleted();
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
|
||||||
|
var user = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "alice",
|
||||||
|
UserName = "alice",
|
||||||
|
Email = "alice@example.test",
|
||||||
|
EmailConfirmed = true,
|
||||||
|
FullName = "Alice",
|
||||||
|
};
|
||||||
|
db.Users.Add(user);
|
||||||
|
|
||||||
|
var location = new Location
|
||||||
|
{
|
||||||
|
Address = "1 rue du Test",
|
||||||
|
Latitude = 48.8566,
|
||||||
|
Longitude = 2.3522,
|
||||||
|
};
|
||||||
|
db.Add(location);
|
||||||
|
db.SaveChanges();
|
||||||
|
|
||||||
|
var activity = new Activity
|
||||||
|
{
|
||||||
|
Code = "dev",
|
||||||
|
Name = "Dev",
|
||||||
|
Hidden = false,
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
db.Activities.Add(activity);
|
||||||
|
|
||||||
|
db.Activities.Add(new Activity
|
||||||
|
{
|
||||||
|
Code = "ghost",
|
||||||
|
Name = "Ghost",
|
||||||
|
Hidden = false,
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
|
var performer = new PerformerProfile
|
||||||
|
{
|
||||||
|
PerformerId = "alice",
|
||||||
|
SIREN = "123456789",
|
||||||
|
OrganizationAddressId = location.Id,
|
||||||
|
AcceptNotifications = true,
|
||||||
|
AcceptPublicContact = true,
|
||||||
|
Active = true,
|
||||||
|
Rate = 5,
|
||||||
|
WebSite = "https://alice.dev",
|
||||||
|
};
|
||||||
|
db.Performers.Add(performer);
|
||||||
|
|
||||||
|
db.UserActivities.Add(new UserActivity
|
||||||
|
{
|
||||||
|
UserId = "alice",
|
||||||
|
DoesCode = "dev",
|
||||||
|
Weight = 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.Users.Add(new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "bob",
|
||||||
|
UserName = "bob",
|
||||||
|
Email = "bob@example.test",
|
||||||
|
EmailConfirmed = true,
|
||||||
|
FullName = "Bob",
|
||||||
|
});
|
||||||
|
|
||||||
|
db.Activities.Add(new Activity
|
||||||
|
{
|
||||||
|
Code = "declared-only",
|
||||||
|
Name = "Declared Only",
|
||||||
|
Hidden = false,
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.Performers.Add(new PerformerProfile
|
||||||
|
{
|
||||||
|
PerformerId = "bob",
|
||||||
|
SIREN = "987654321",
|
||||||
|
OrganizationAddressId = location.Id,
|
||||||
|
AcceptNotifications = false,
|
||||||
|
AcceptPublicContact = false,
|
||||||
|
Active = false,
|
||||||
|
Rate = 0,
|
||||||
|
WebSite = "",
|
||||||
|
});
|
||||||
|
|
||||||
|
db.UserActivities.Add(new UserActivity
|
||||||
|
{
|
||||||
|
UserId = "bob",
|
||||||
|
DoesCode = "declared-only",
|
||||||
|
Weight = 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_sqliteLock)
|
||||||
|
{
|
||||||
|
if (_sharedSqliteConnection is not null)
|
||||||
|
{
|
||||||
|
_sharedSqliteConnection.Close();
|
||||||
|
_sharedSqliteConnection.Dispose();
|
||||||
|
_sharedSqliteConnection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/Yavsc.Api.Test/HomeControllerTests.cs
Normal file
69
src/Yavsc.Api.Test/HomeControllerTests.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Yavsc.Api.Test.Fixtures;
|
||||||
|
using Yavsc.Controllers;
|
||||||
|
using Yavsc.Models.Workflow;
|
||||||
|
|
||||||
|
namespace Yavsc.Api.Test;
|
||||||
|
|
||||||
|
[Collection("Yavsc Api")]
|
||||||
|
public sealed class HomeControllerTests : IClassFixture<ApiWebServerFixture>
|
||||||
|
{
|
||||||
|
private readonly ApiWebServerFixture _fixture;
|
||||||
|
|
||||||
|
public HomeControllerTests(ApiWebServerFixture fixture)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Index_does_not_list_activity_without_declaration()
|
||||||
|
{
|
||||||
|
_fixture.ResetAndSeedActivityGraph();
|
||||||
|
|
||||||
|
using var scope = _fixture.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<Yavsc.Models.ApplicationDbContext>();
|
||||||
|
|
||||||
|
var controller = new HomeController(
|
||||||
|
NullLogger<HomeController>.Instance,
|
||||||
|
localizer: null!,
|
||||||
|
context: db,
|
||||||
|
settingsOptions: Options.Create(new SiteSettings()),
|
||||||
|
env: new TestEnvironment());
|
||||||
|
|
||||||
|
controller.ControllerContext = new ControllerContext
|
||||||
|
{
|
||||||
|
HttpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
User = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
||||||
|
{
|
||||||
|
new Claim("sub", "alice"),
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, "alice")
|
||||||
|
}, "Bearer"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await controller.Index(id: null);
|
||||||
|
var view = Assert.IsType<ViewResult>(result);
|
||||||
|
var model = Assert.IsAssignableFrom<IEnumerable<Activity>>(view.Model);
|
||||||
|
|
||||||
|
Assert.Contains(model, a => a.Code == "dev");
|
||||||
|
Assert.DoesNotContain(model, a => a.Code == "ghost");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestEnvironment : IWebHostEnvironment
|
||||||
|
{
|
||||||
|
public string ApplicationName { get; set; } = "Yavsc.Api.Test";
|
||||||
|
public IFileProvider WebRootFileProvider { get; set; } = null!;
|
||||||
|
public string WebRootPath { get; set; } = string.Empty;
|
||||||
|
public string EnvironmentName { get; set; } = "Development";
|
||||||
|
public string ContentRootPath { get; set; } = string.Empty;
|
||||||
|
public IFileProvider ContentRootFileProvider { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Yavsc.Api.Test/Yavsc.Api.Test.csproj
Normal file
33
src/Yavsc.Api.Test/Yavsc.Api.Test.csproj
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<RootNamespace>Yavsc.Api.Test</RootNamespace>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
|
<PackageReference Include="xunit.v3" />
|
||||||
|
<PackageReference Include="xunit.v3.common" />
|
||||||
|
<PackageReference Include="xunit.v3.extensibility.core" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Yavsc.Abstract\Yavsc.Abstract.csproj" />
|
||||||
|
<ProjectReference Include="..\Yavsc.Server\Yavsc.Server.csproj" />
|
||||||
|
<ProjectReference Include="..\Yavsc.Api\Yavsc.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\Yavsc.Org\Yavsc.Org.csproj" />
|
||||||
|
<ProjectReference Include="..\Yavsc.Tests.Shared\Yavsc.Tests.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -53,8 +53,7 @@ namespace Yavsc.Controllers
|
||||||
|
|
||||||
var performerCounts = await (
|
var performerCounts = await (
|
||||||
from ua in _context.UserActivities.AsNoTracking()
|
from ua in _context.UserActivities.AsNoTracking()
|
||||||
join p in _context.Performers.AsNoTracking() on ua.UserId equals p.PerformerId
|
where !string.IsNullOrWhiteSpace(ua.DoesCode) && codes.Contains(ua.DoesCode)
|
||||||
where p.Active && !string.IsNullOrWhiteSpace(ua.DoesCode) && codes.Contains(ua.DoesCode)
|
|
||||||
group ua by ua.DoesCode into g
|
group ua by ua.DoesCode into g
|
||||||
select new
|
select new
|
||||||
{
|
{
|
||||||
|
|
@ -63,11 +62,19 @@ namespace Yavsc.Controllers
|
||||||
})
|
})
|
||||||
.ToDictionaryAsync(x => x.Code, x => x.Count, cancellationToken);
|
.ToDictionaryAsync(x => x.Code, x => x.Count, cancellationToken);
|
||||||
|
|
||||||
return Ok(activities.Select(a => ToBrowseItem(a, performerCounts)).ToList());
|
var filteredActivities = activities
|
||||||
|
.Where(a =>
|
||||||
|
(performerCounts.TryGetValue(a.Code, out var ownCount) && ownCount > 0)
|
||||||
|
|| (a.Children ?? new List<Activity>())
|
||||||
|
.Where(c => !c.Hidden)
|
||||||
|
.Any(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(filteredActivities.Select(a => ToBrowseItem(a, performerCounts)).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/performers")]
|
[HttpGet("{id}/users")]
|
||||||
public async Task<ActionResult<IEnumerable<ActivityPerformerDto>>> GetPerformers(
|
public async Task<ActionResult<IEnumerable<ActivityPerformerDto>>> GetUsers(
|
||||||
[FromRoute] string id,
|
[FromRoute] string id,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
@ -84,35 +91,54 @@ namespace Yavsc.Controllers
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var performers = await (
|
var users = await QueryDeclaredUsersAsync(id, activity.Name, cancellationToken);
|
||||||
from p in _context.Performers.AsNoTracking()
|
|
||||||
join ua in _context.UserActivities.AsNoTracking() on p.PerformerId equals ua.UserId
|
return Ok(users);
|
||||||
join u in _context.ApplicationUser.AsNoTracking() on p.PerformerId equals u.Id into users
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}/performers")]
|
||||||
|
public Task<ActionResult<IEnumerable<ActivityPerformerDto>>> GetPerformers(
|
||||||
|
[FromRoute] string id,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Backward-compatible alias kept for existing clients.
|
||||||
|
return GetUsers(id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<List<ActivityPerformerDto>> QueryDeclaredUsersAsync(
|
||||||
|
string activityCode,
|
||||||
|
string activityName,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
from ua in _context.UserActivities.AsNoTracking()
|
||||||
|
join u in _context.ApplicationUser.AsNoTracking() on ua.UserId equals u.Id into users
|
||||||
from user in users.DefaultIfEmpty()
|
from user in users.DefaultIfEmpty()
|
||||||
where p.Active && ua.DoesCode == id
|
join p in _context.Performers.AsNoTracking() on ua.UserId equals p.PerformerId into performerProfiles
|
||||||
orderby p.Rate
|
from performer in performerProfiles.DefaultIfEmpty()
|
||||||
|
where ua.DoesCode == activityCode
|
||||||
|
orderby user != null ? user.UserName : ua.UserId
|
||||||
select new ActivityPerformerDto
|
select new ActivityPerformerDto
|
||||||
{
|
{
|
||||||
PerformerId = p.PerformerId,
|
PerformerId = ua.UserId,
|
||||||
|
HasPerformerProfile = performer != null,
|
||||||
UserName = user != null ? (user.UserName ?? string.Empty) : string.Empty,
|
UserName = user != null ? (user.UserName ?? string.Empty) : string.Empty,
|
||||||
Active = p.Active,
|
Active = performer != null && performer.Active,
|
||||||
AcceptNotifications = p.AcceptNotifications,
|
AcceptNotifications = performer != null && performer.AcceptNotifications,
|
||||||
AcceptPublicContact = p.AcceptPublicContact,
|
AcceptPublicContact = performer != null && performer.AcceptPublicContact,
|
||||||
WebSite = p.WebSite ?? string.Empty,
|
WebSite = performer != null ? (performer.WebSite ?? string.Empty) : string.Empty,
|
||||||
ActivityCode = id,
|
ActivityCode = activityCode,
|
||||||
ActivityName = activity.Name,
|
ActivityName = activityName,
|
||||||
SettingsClassName = _context.Activities
|
SettingsClassName = _context.Activities
|
||||||
.Where(a => a.Code == id)
|
.Where(a => a.Code == activityCode)
|
||||||
.Select(a => a.SettingsClassName)
|
.Select(a => a.SettingsClassName)
|
||||||
.FirstOrDefault() ?? string.Empty,
|
.FirstOrDefault() ?? string.Empty,
|
||||||
ExtraActivityCount = _context.UserActivities
|
ExtraActivityCount = _context.UserActivities
|
||||||
.Where(x => x.UserId == p.PerformerId && x.DoesCode != id)
|
.Where(x => x.UserId == ua.UserId && x.DoesCode != activityCode)
|
||||||
.Count()
|
.Count()
|
||||||
})
|
})
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
return Ok(performers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/ActivityApi/5
|
// GET: api/ActivityApi/5
|
||||||
|
|
@ -257,6 +283,7 @@ namespace Yavsc.Controllers
|
||||||
.ToList(),
|
.ToList(),
|
||||||
Children = (activity.Children ?? Enumerable.Empty<Activity>())
|
Children = (activity.Children ?? Enumerable.Empty<Activity>())
|
||||||
.Where(c => !c.Hidden)
|
.Where(c => !c.Hidden)
|
||||||
|
.Where(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0)
|
||||||
.OrderByDescending(c => c.Rate)
|
.OrderByDescending(c => c.Rate)
|
||||||
.Select(c => new ActivityBrowseItemDto
|
.Select(c => new ActivityBrowseItemDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Billing;
|
using Yavsc.Models.Billing;
|
||||||
using Yavsc.Server.Helpers;
|
using Yavsc.Server.Helpers;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Yavsc.Controllers
|
namespace Yavsc.Controllers
|
||||||
{
|
{
|
||||||
|
|
@ -13,7 +15,7 @@ namespace Yavsc.Controllers
|
||||||
|
|
||||||
public SIRENExceptionsController(ApplicationDbContext context)
|
public SIRENExceptionsController(ApplicationDbContext context)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: SIRENExceptions
|
// GET: SIRENExceptions
|
||||||
|
|
@ -50,15 +52,41 @@ namespace Yavsc.Controllers
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public IActionResult Create(ExceptionSIREN exceptionSIREN)
|
public IActionResult Create(ExceptionSIREN exceptionSIREN)
|
||||||
{
|
{
|
||||||
|
exceptionSIREN ??= new ExceptionSIREN();
|
||||||
|
exceptionSIREN.SIREN = NormalizeSiren(exceptionSIREN.SIREN);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(exceptionSIREN.SIREN) || exceptionSIREN.SIREN.Length != 9 || !exceptionSIREN.SIREN.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(ExceptionSIREN.SIREN), "Le SIREN doit contenir exactement 9 chiffres.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_context.ExceptionsSIREN.Any(e => e.SIREN == exceptionSIREN.SIREN))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(ExceptionSIREN.SIREN), "Ce SIREN est deja dans la liste des exceptions.");
|
||||||
|
}
|
||||||
|
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
_context.ExceptionsSIREN.Add(exceptionSIREN);
|
_context.ExceptionsSIREN.Add(exceptionSIREN);
|
||||||
_context.SaveChanges(User.GetUserId());
|
try
|
||||||
return RedirectToAction("Index");
|
{
|
||||||
|
_context.SaveChanges(User.GetUserId());
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty, "Impossible d'enregistrer cette exception SIREN.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return View(exceptionSIREN);
|
return View(exceptionSIREN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeSiren(string? siren)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(siren)) return string.Empty;
|
||||||
|
return new string(siren.Where(char.IsDigit).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
// GET: SIRENExceptions/Edit/5
|
// GET: SIRENExceptions/Edit/5
|
||||||
public IActionResult Edit(string id)
|
public IActionResult Edit(string id)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,35 @@ namespace Yavsc.Controllers
|
||||||
.Where(a => a.ParentCode == id)
|
.Where(a => a.ParentCode == id)
|
||||||
.OrderByDescending(a => a.Rate).ToList();
|
.OrderByDescending(a => a.Rate).ToList();
|
||||||
|
|
||||||
|
var candidateCodes = toShow
|
||||||
|
.Select(a => a.Code)
|
||||||
|
.Concat(toShow.SelectMany(a => (a.Children ?? new List<Yavsc.Models.Workflow.Activity>())
|
||||||
|
.Where(c => !c.Hidden)
|
||||||
|
.Select(c => c.Code)))
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c))
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var performerCounts = _dbContext.UserActivities
|
||||||
|
.Where(ua => candidateCodes.Contains(ua.DoesCode))
|
||||||
|
.GroupBy(ua => ua.DoesCode)
|
||||||
|
.Select(g => new { Code = g.Key, Count = g.Select(x => x.UserId).Distinct().Count() })
|
||||||
|
.ToDictionary(x => x.Code, x => x.Count);
|
||||||
|
|
||||||
|
toShow = toShow
|
||||||
|
.Where(a =>
|
||||||
|
(performerCounts.TryGetValue(a.Code, out var ownCount) && ownCount > 0)
|
||||||
|
|| (a.Children ?? new List<Yavsc.Models.Workflow.Activity>())
|
||||||
|
.Where(c => !c.Hidden)
|
||||||
|
.Any(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
foreach (var a in toShow)
|
foreach (var a in toShow)
|
||||||
{
|
{
|
||||||
a.Children = a.Children.Where(c => !c.Hidden).ToList();
|
a.Children = (a.Children ?? new List<Yavsc.Models.Workflow.Activity>())
|
||||||
|
.Where(c => !c.Hidden)
|
||||||
|
.Where(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
return View(toShow);
|
return View(toShow);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
<label asp-for="Active" class="col-md-2 control-label">ActivateMyProSettings</label>
|
<label asp-for="Active" class="col-md-2 control-label">ActivateMyProSettings</label>
|
||||||
<div>
|
<div>
|
||||||
<input asp-for="Active" class="form-control" />
|
<input class="form-check-input" type="checkbox" asp-for="Active" class="form-control" />
|
||||||
|
|
||||||
<span asp-validation-for="Active" class="text-danger"></span>
|
<span asp-validation-for="Active" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
<form asp-action="Create" method="post">
|
<form asp-action="Create" method="post">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<h4>Exception SIREN</h4>
|
<h4>Exception SIREN</h4>
|
||||||
<hr />
|
<hr />
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label asp-for="SIREN" class="control-label">SIREN</label>
|
||||||
|
<input asp-for="SIREN" class="form-control" maxlength="9" pattern="[0-9]{9}" inputmode="numeric" autocomplete="off" />
|
||||||
|
<span asp-validation-for="SIREN" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@Html.TextBox("SIREN")
|
<input type="submit" value="Create" class="btn btn-primary" />
|
||||||
<div class="col-md-offset-1 col-md-10">
|
|
||||||
<div class="col-md-offset-2 col-md-10">
|
|
||||||
<input type="submit" value="Create" class="btn btn-primary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">Back to List</a>
|
<a asp-action="Index">Back to List</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
|
||||||
|
}
|
||||||
|
|
|
||||||
15
yavsc.sln
15
yavsc.sln
|
|
@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Api.Client", "src\Yav
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Tests", "src\PostIt\PostIt.Tests\PostIt.Tests.csproj", "{021E6F5A-B81D-42A7-9918-DFE39B424FC2}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Tests", "src\PostIt\PostIt.Tests\PostIt.Tests.csproj", "{021E6F5A-B81D-42A7-9918-DFE39B424FC2}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Api.Test", "src\Yavsc.Api.Test\Yavsc.Api.Test.csproj", "{49A42F0E-6E18-4617-9716-3E728966D96F}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -229,6 +231,18 @@ Global
|
||||||
{021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x64.Build.0 = Release|Any CPU
|
{021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x86.ActiveCfg = Release|Any CPU
|
{021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x86.Build.0 = Release|Any CPU
|
{021E6F5A-B81D-42A7-9918-DFE39B424FC2}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -250,5 +264,6 @@ Global
|
||||||
{34D1F73D-BF74-47CC-9358-9F4F221C75D7} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2}
|
{34D1F73D-BF74-47CC-9358-9F4F221C75D7} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2}
|
||||||
{59AF5DEA-D349-495A-BC44-FC7BD4E55099} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2}
|
{59AF5DEA-D349-495A-BC44-FC7BD4E55099} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2}
|
||||||
{021E6F5A-B81D-42A7-9918-DFE39B424FC2} = {E13D107F-4053-D0DE-6394-453609595BFE}
|
{021E6F5A-B81D-42A7-9918-DFE39B424FC2} = {E13D107F-4053-D0DE-6394-453609595BFE}
|
||||||
|
{49A42F0E-6E18-4617-9716-3E728966D96F} = {76A7FBA6-B7EC-4864-85C0-1F470794BA0F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue