-
-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/hjemlo #507
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Feature/hjemlo #507
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
9379555
add hjemlo integration
NielsPilgaard f016fd4
tests & fixes
NielsPilgaard a68ca75
simplify massively, do things manually
NielsPilgaard d50ab84
Delete HjemGroupScraperServiceTests.cs
NielsPilgaard bf1a3a3
Delete HjemGroupIntegrationTests.cs
NielsPilgaard c6fc028
Create HjemGroupAdminServiceTests.cs
NielsPilgaard 2442389
review comments
NielsPilgaard 7efdc82
Update HjemGroupAdminServiceTests.cs
NielsPilgaard c3902c4
Update NotificationItem.razor
NielsPilgaard dc4c9b4
fixes
NielsPilgaard 8357277
Update NotificationItem.razor.css
NielsPilgaard 91602e8
Create logo-hjem.avif
NielsPilgaard ee72b73
finish icon url part of the feature
NielsPilgaard 4cb6dc2
update caching, fix tests
NielsPilgaard e9a544d
styling and tests
NielsPilgaard ac8e4d9
Update HjemGroupManagementPage.razor
NielsPilgaard 63cdd47
Update HjemGroupAdminServiceTests.cs
NielsPilgaard 2c9d77f
Create 13-cheaper-infrastructure.md
NielsPilgaard f18fe2d
Merge branch 'main' into feature/hjemlo
NielsPilgaard 590342d
review comments
NielsPilgaard 0dc7aa0
review fixes
NielsPilgaard 6229e8a
review comments
NielsPilgaard a2a3e3f
review comments
NielsPilgaard 036bc26
fixes
NielsPilgaard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
src/web/Jordnaer/Features/HjemGroups/HjemGroupAdminService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| using Azure.Storage.Blobs; | ||
| using Azure.Storage.Blobs.Models; | ||
| using Jordnaer.Shared; | ||
| using MassTransit; | ||
| using OneOf; | ||
| using OneOf.Types; | ||
| using System.Text; | ||
| using System.Text.Json; | ||
|
|
||
| namespace Jordnaer.Features.HjemGroups; | ||
|
|
||
| public class HjemGroupAdminService( | ||
| BlobServiceClient blobServiceClient, | ||
| IDataForsyningenClient dataForsyningenClient, | ||
| IPublishEndpoint publishEndpoint, | ||
| ILogger<HjemGroupAdminService> logger) | ||
| { | ||
| private const string ContainerName = "hjemlo-groups"; | ||
| private const string BlobName = "groups.json"; | ||
|
|
||
| private static readonly JsonSerializerOptions JsonOptions = new() | ||
| { | ||
| WriteIndented = true, | ||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||
| }; | ||
|
|
||
| public async Task<OneOf<List<HjemGroupEntry>, HjemGroupLoadError>> LoadAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| try | ||
| { | ||
| var containerClient = blobServiceClient.GetBlobContainerClient(ContainerName); | ||
| if (!await containerClient.ExistsAsync(cancellationToken)) | ||
| { | ||
| return new HjemGroupLoadError($"Blob container '{ContainerName}' does not exist."); | ||
| } | ||
|
|
||
| var blobClient = containerClient.GetBlobClient(BlobName); | ||
| if (!await blobClient.ExistsAsync(cancellationToken)) | ||
| { | ||
| return new HjemGroupLoadError($"Blob '{BlobName}' does not exist in container '{ContainerName}'."); | ||
| } | ||
|
|
||
| var response = await blobClient.DownloadContentAsync(cancellationToken); | ||
| return JsonSerializer.Deserialize<List<HjemGroupEntry>>( | ||
| response.Value.Content.ToString(), JsonOptions) ?? []; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogWarning(ex, "Failed to load HJEM group entries from blob storage."); | ||
| return new HjemGroupLoadError(ex.Message); | ||
| } | ||
| } | ||
|
|
||
| public async Task<OneOf<Success, HjemGroupSaveError>> SaveAsync(List<HjemGroupEntry> entries, CancellationToken cancellationToken = default) | ||
| { | ||
| try | ||
| { | ||
| var containerClient = blobServiceClient.GetBlobContainerClient(ContainerName); | ||
| await containerClient.CreateIfNotExistsAsync(PublicAccessType.None, cancellationToken: cancellationToken); | ||
|
|
||
| var blobClient = containerClient.GetBlobClient(BlobName); | ||
| var json = JsonSerializer.Serialize(entries, JsonOptions); | ||
|
|
||
| using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); | ||
| await blobClient.UploadAsync(stream, overwrite: true, cancellationToken: cancellationToken); | ||
|
|
||
| await publishEndpoint.Publish( | ||
| new InvalidateCacheTags { Tags = [HjemGroupProvider.CacheTag] }, | ||
| cancellationToken); | ||
|
|
||
| return new Success(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogError(ex, "Failed to save HJEM group entries."); | ||
| return new HjemGroupSaveError(ex.Message); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Looks up coordinates and city/zip for a Danish city or area name via Dataforsyningen. | ||
| /// Returns null if not found. | ||
| /// </summary> | ||
| public async Task<GeocodeResult?> GeocodeAsync(string locationText, CancellationToken cancellationToken = default) | ||
| { | ||
| var response = await dataForsyningenClient.SearchZipCodesAsync(locationText, cancellationToken); | ||
|
|
||
| if (!response.IsSuccessStatusCode || response.Content is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var first = response.Content.FirstOrDefault(); | ||
| if (first.Navn is null || first.Visueltcenter is not { Length: >= 2 }) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| // GeoJSON order: [longitude, latitude] | ||
| var longitude = (double)first.Visueltcenter[0]; | ||
| var latitude = (double)first.Visueltcenter[1]; | ||
|
|
||
| int? zipCode = null; | ||
| if (int.TryParse(first.Nr, out var parsedZip)) | ||
| { | ||
| zipCode = parsedZip; | ||
| } | ||
|
|
||
| return new GeocodeResult(first.Navn, zipCode, latitude, longitude); | ||
| } | ||
|
|
||
| public sealed record GeocodeResult(string City, int? ZipCode, double Latitude, double Longitude); | ||
| } | ||
|
|
||
| public sealed record HjemGroupLoadError(string Message); | ||
| public sealed record HjemGroupSaveError(string Message); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| namespace Jordnaer.Features.HjemGroups; | ||
|
|
||
| public record HjemGroupEntry | ||
| { | ||
| public required string Name { get; init; } | ||
| public required Uri WebsiteUrl { get; init; } | ||
| public string? City { get; init; } | ||
| public int? ZipCode { get; init; } | ||
| public required double Latitude { get; init; } | ||
| public required double Longitude { get; init; } | ||
| public required HjemGroupType Type { get; init; } | ||
| public string? IconUrl { get; init; } | ||
| } | ||
|
|
||
| public enum HjemGroupType { Lokalafdeling, Lokalrepresentant } |
121 changes: 121 additions & 0 deletions
121
src/web/Jordnaer/Features/HjemGroups/HjemGroupProvider.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| using Azure.Storage.Blobs; | ||
| using Jordnaer.Features.Map; | ||
| using OneOf; | ||
| using OneOf.Types; | ||
| using System.Security.Cryptography; | ||
| using System.Text; | ||
| using System.Text.Json; | ||
| using ZiggyCreatures.Caching.Fusion; | ||
|
|
||
| namespace Jordnaer.Features.HjemGroups; | ||
|
|
||
| public interface IHjemGroupProvider | ||
| { | ||
| Task<OneOf<IReadOnlyList<GroupMarkerData>, Error>> GetMarkersAsync(CancellationToken cancellationToken = default); | ||
| } | ||
|
|
||
| public class HjemGroupProvider( | ||
| BlobServiceClient blobServiceClient, | ||
| IFusionCache fusionCache, | ||
| ILogger<HjemGroupProvider> logger) : IHjemGroupProvider | ||
| { | ||
| internal const string CacheTag = "hjem-groups"; | ||
| private const string CacheKey = "HjemGroups:markers"; | ||
| private const string ContainerName = "hjemlo-groups"; | ||
| private const string BlobName = "groups.json"; | ||
|
|
||
| private static readonly JsonSerializerOptions JsonOptions = new() | ||
| { | ||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||
| }; | ||
|
|
||
| public async Task<OneOf<IReadOnlyList<GroupMarkerData>, Error>> GetMarkersAsync(CancellationToken cancellationToken = default) | ||
| { | ||
| try | ||
| { | ||
| var markers = await fusionCache.GetOrSetAsync<IReadOnlyList<GroupMarkerData>>( | ||
| CacheKey, | ||
| async (_, innerToken) => | ||
| { | ||
| var result = await LoadFromBlobAsync(innerToken); | ||
| return result.IsT0 | ||
| ? result.AsT0 | ||
| : throw new InvalidOperationException("Failed to load HJEM group markers from blob storage."); | ||
| }, | ||
| tags: [CacheTag], | ||
| token: cancellationToken); | ||
| return OneOf<IReadOnlyList<GroupMarkerData>, Error>.FromT0(markers); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| throw; | ||
| } | ||
| catch (Exception) | ||
| { | ||
| return new Error(); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
NielsPilgaard marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| private async Task<OneOf<IReadOnlyList<GroupMarkerData>, Error>> LoadFromBlobAsync(CancellationToken cancellationToken) | ||
| { | ||
| try | ||
| { | ||
| var containerClient = blobServiceClient.GetBlobContainerClient(ContainerName); | ||
|
|
||
| if (!await containerClient.ExistsAsync(cancellationToken)) | ||
| { | ||
| logger.LogError("Blob container '{ContainerName}' does not exist.", ContainerName); | ||
| return new Error(); | ||
| } | ||
|
|
||
| var blobClient = containerClient.GetBlobClient(BlobName); | ||
|
|
||
| if (!await blobClient.ExistsAsync(cancellationToken)) | ||
| { | ||
| logger.LogError("Blob '{BlobName}' does not exist in container '{ContainerName}'.", BlobName, ContainerName); | ||
| return new Error(); | ||
| } | ||
|
|
||
| var response = await blobClient.DownloadContentAsync(cancellationToken); | ||
| var entries = JsonSerializer.Deserialize<List<HjemGroupEntry>>( | ||
| response.Value.Content.ToString(), JsonOptions); | ||
|
|
||
| if (entries is null or { Count: 0 }) | ||
| { | ||
| return Array.Empty<GroupMarkerData>(); | ||
| } | ||
|
|
||
| return entries.Select(MapToMarker).ToList(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogError(ex, "Failed to load HJEM group markers from blob storage."); | ||
| return new Error(); | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| private static GroupMarkerData MapToMarker(HjemGroupEntry entry) | ||
| { | ||
| var idBytes = MD5.HashData(Encoding.UTF8.GetBytes(entry.WebsiteUrl.ToString() + entry.Name)); | ||
| var id = new Guid(idBytes); | ||
|
|
||
| var websiteUrl = entry.Type == HjemGroupType.Lokalrepresentant | ||
| ? "https://www.hjemlo.dk/lokalrepraesentanter" | ||
| : entry.WebsiteUrl.ToString(); | ||
|
|
||
| return new GroupMarkerData | ||
| { | ||
| Id = id, | ||
| Name = entry.Name, | ||
| ProfilePictureUrl = entry.IconUrl ?? "/images/partners/logo-hjem.avif", | ||
| WebsiteUrl = websiteUrl, | ||
| ShortDescription = entry.Type == HjemGroupType.Lokalafdeling | ||
| ? "HJEM lokalafdeling" | ||
| : "HJEM lokalrepræsentant", | ||
| ZipCode = entry.ZipCode, | ||
| City = entry.City, | ||
| Latitude = entry.Latitude, | ||
| Longitude = entry.Longitude, | ||
| }; | ||
| } | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
src/web/Jordnaer/Features/HjemGroups/WebApplicationBuilderExtensions.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| namespace Jordnaer.Features.HjemGroups; | ||
|
|
||
| public static class WebApplicationBuilderExtensions | ||
| { | ||
| public static WebApplicationBuilder AddHjemGroupServices(this WebApplicationBuilder builder) | ||
| { | ||
| builder.Services.AddScoped<IHjemGroupProvider, HjemGroupProvider>(); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| builder.Services.AddScoped<HjemGroupAdminService>(); | ||
|
|
||
| return builder; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.