-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
make 90% of the backend for posts (not groupposts yet)
- Loading branch information
1 parent
abd8d32
commit 8b0ca9d
Showing
13 changed files
with
392 additions
and
8 deletions.
There are no files selected for viewing
This file contains 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 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,30 @@ | ||
using System.ComponentModel.DataAnnotations.Schema; | ||
using System.ComponentModel.DataAnnotations; | ||
|
||
namespace Jordnaer.Shared; | ||
|
||
public class GroupPost | ||
{ | ||
|
||
[Key] | ||
[DatabaseGenerated(DatabaseGeneratedOption.None)] | ||
public required Guid Id { get; init; } | ||
|
||
[StringLength(1000, ErrorMessage = "Opslag må højest være 1000 karakterer lang.")] | ||
[Required(AllowEmptyStrings = false, ErrorMessage = "Opslag skal have mindst 1 karakter.")] | ||
public required string Text { get; init; } | ||
|
||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow; | ||
|
||
public int? ZipCode { get; set; } | ||
|
||
[ForeignKey(nameof(UserProfile))] | ||
public required string UserProfileId { get; init; } = null!; | ||
|
||
public UserProfile UserProfile { get; init; } = null!; | ||
|
||
[ForeignKey(nameof(Group))] | ||
public required Guid GroupId { get; init; } | ||
|
||
public Group Group { get; init; } = null!; | ||
} |
This file contains 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,27 @@ | ||
using System.ComponentModel.DataAnnotations; | ||
using System.ComponentModel.DataAnnotations.Schema; | ||
|
||
namespace Jordnaer.Shared; | ||
|
||
public class Post | ||
{ | ||
[Key] | ||
[DatabaseGenerated(DatabaseGeneratedOption.None)] | ||
public required Guid Id { get; init; } | ||
|
||
[StringLength(1000, ErrorMessage = "Opslag må højest være 1000 karakterer lang.")] | ||
[Required(AllowEmptyStrings = false, ErrorMessage = "Opslag skal have mindst 1 karakter.")] | ||
public required string Text { get; init; } | ||
|
||
public DateTimeOffset CreatedUtc { get; init; } = DateTimeOffset.UtcNow; | ||
|
||
public int? ZipCode { get; set; } | ||
public string? City { get; set; } | ||
|
||
[ForeignKey(nameof(UserProfile))] | ||
public required string UserProfileId { get; init; } = null!; | ||
|
||
public UserProfile UserProfile { get; init; } = null!; | ||
|
||
public List<Category> Categories { get; set; } = []; | ||
} |
This file contains 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,8 @@ | ||
namespace Jordnaer.Shared; | ||
|
||
public class PostCategory | ||
{ | ||
public required Guid PostId { get; set; } | ||
|
||
public required int CategoryId { get; set; } | ||
} |
This file contains 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,24 @@ | ||
namespace Jordnaer.Shared; | ||
|
||
public static class PostExtensions | ||
{ | ||
public static PostDto ToPostDto(this Post post) | ||
{ | ||
return new PostDto | ||
{ | ||
Id = post.Id, | ||
Text = post.Text, | ||
CreatedUtc = post.CreatedUtc, | ||
Author = new UserSlim | ||
{ | ||
Id = post.UserProfileId, | ||
ProfilePictureUrl = post.UserProfile.ProfilePictureUrl, | ||
UserName = post.UserProfile.UserName, | ||
DisplayName = post.UserProfile.DisplayName | ||
}, | ||
City = post.City, | ||
ZipCode = post.ZipCode, | ||
Categories = post.Categories.Select(category => category.Name).ToList() | ||
}; | ||
} | ||
} |
This file contains 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,21 @@ | ||
using System.ComponentModel.DataAnnotations; | ||
|
||
namespace Jordnaer.Shared; | ||
|
||
public class PostDto | ||
{ | ||
public required Guid Id { get; init; } | ||
|
||
[StringLength(1000, ErrorMessage = "Opslag må højest være 1000 karakterer lang.")] | ||
[Required(AllowEmptyStrings = false, ErrorMessage = "Opslag skal have mindst 1 karakter.")] | ||
public required string Text { get; init; } | ||
|
||
public int? ZipCode { get; set; } | ||
public string? City { get; set; } | ||
|
||
public DateTimeOffset CreatedUtc { get; init; } | ||
|
||
public required UserSlim Author { get; init; } | ||
|
||
public List<string> Categories { get; set; } = []; | ||
} |
This file contains 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,57 @@ | ||
using System.ComponentModel.DataAnnotations; | ||
|
||
namespace Jordnaer.Shared; | ||
|
||
public class PostSearchFilter | ||
{ | ||
public string? Contents { get; set; } | ||
public string[]? Categories { get; set; } = []; | ||
|
||
/// <summary> | ||
/// Only show user results within this many kilometers of the <see cref="Location"/>. | ||
/// </summary> | ||
[Range(1, 50, ErrorMessage = "Afstand skal være mellem 1 og 50 km")] | ||
[LocationRequired] | ||
public int? WithinRadiusKilometers { get; set; } | ||
|
||
[RadiusRequired] | ||
public string? Location { get; set; } | ||
|
||
public int PageNumber { get; set; } = 1; | ||
public int PageSize { get; set; } = 10; | ||
} | ||
|
||
file class RadiusRequiredAttribute : ValidationAttribute | ||
{ | ||
protected override ValidationResult IsValid(object? value, ValidationContext validationContext) | ||
{ | ||
var postSearchFilter = (PostSearchFilter)validationContext.ObjectInstance; | ||
|
||
if (postSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(postSearchFilter.Location)) | ||
{ | ||
return ValidationResult.Success!; | ||
} | ||
|
||
return postSearchFilter.WithinRadiusKilometers is null | ||
? new ValidationResult("Radius skal vælges når et område er valgt.") | ||
: ValidationResult.Success!; | ||
} | ||
} | ||
|
||
file class LocationRequiredAttribute : ValidationAttribute | ||
{ | ||
protected override ValidationResult IsValid(object? value, ValidationContext validationContext) | ||
{ | ||
var postSearchFilter = (PostSearchFilter)validationContext.ObjectInstance; | ||
|
||
if (postSearchFilter.WithinRadiusKilometers is null && string.IsNullOrEmpty(postSearchFilter.Location)) | ||
{ | ||
return ValidationResult.Success!; | ||
|
||
} | ||
|
||
return string.IsNullOrEmpty(postSearchFilter.Location) | ||
? new ValidationResult("Område skal vælges når en radius er valgt.") | ||
: ValidationResult.Success!; | ||
} | ||
} |
This file contains 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,7 @@ | ||
namespace Jordnaer.Shared; | ||
|
||
public class PostSearchResult | ||
{ | ||
public List<PostDto> Posts { get; set; } = []; | ||
public int TotalCount { get; set; } | ||
} |
This file contains 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 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
109 changes: 109 additions & 0 deletions
109
src/web/Jordnaer/Features/PostSearch/PostSearchService.cs
This file contains 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,109 @@ | ||
using Jordnaer.Database; | ||
using Jordnaer.Features.Metrics; | ||
using Jordnaer.Features.Search; | ||
using Jordnaer.Shared; | ||
using Microsoft.EntityFrameworkCore; | ||
|
||
namespace Jordnaer.Features.PostSearch; | ||
|
||
public interface IPostSearchService | ||
{ | ||
Task<PostSearchResult> GetPostsAsync(PostSearchFilter filter, | ||
CancellationToken cancellationToken = default); | ||
} | ||
|
||
public class PostSearchService( | ||
IDbContextFactory<JordnaerDbContext> contextFactory, | ||
IZipCodeService zipCodeService) : IPostSearchService | ||
{ | ||
public async Task<PostSearchResult> GetPostsAsync(PostSearchFilter filter, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
JordnaerMetrics.PostSearchesCounter.Add(1, MakeTagList(filter)); | ||
|
||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); | ||
|
||
var query = context.Posts | ||
.AsNoTracking() | ||
.AsQueryable(); | ||
|
||
query = ApplyCategoryFilter(filter, query); | ||
query = await ApplyLocationFilterAsync(filter, query, cancellationToken); | ||
query = ApplyContentFilter(filter.Contents, query); | ||
|
||
var postsToSkip = filter.PageNumber == 1 | ||
? 0 | ||
: (filter.PageNumber - 1) * filter.PageSize; | ||
|
||
var posts = await query.OrderByDescending(x => x.CreatedUtc) | ||
.Skip(postsToSkip) | ||
.Take(filter.PageSize) | ||
.Select(x => x.ToPostDto()) | ||
.ToListAsync(cancellationToken); | ||
|
||
var totalCount = await query.CountAsync(cancellationToken); | ||
|
||
return new PostSearchResult | ||
{ | ||
Posts = posts, | ||
TotalCount = totalCount | ||
}; | ||
} | ||
|
||
internal async Task<IQueryable<Post>> ApplyLocationFilterAsync( | ||
PostSearchFilter filter, | ||
IQueryable<Post> posts, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
if (string.IsNullOrEmpty(filter.Location) || filter.WithinRadiusKilometers is null) | ||
{ | ||
return posts; | ||
} | ||
|
||
var (zipCodesWithinCircle, searchedZipCode) = await zipCodeService.GetZipCodesNearLocationAsync( | ||
filter.Location, | ||
filter.WithinRadiusKilometers.Value, | ||
cancellationToken); | ||
|
||
if (zipCodesWithinCircle.Count is 0 || searchedZipCode is null) | ||
{ | ||
return posts; | ||
} | ||
|
||
posts = posts.Where(user => user.ZipCode != null && | ||
zipCodesWithinCircle.Contains(user.ZipCode.Value)); | ||
|
||
return posts; | ||
} | ||
|
||
internal static IQueryable<Post> ApplyCategoryFilter(PostSearchFilter filter, IQueryable<Post> posts) | ||
{ | ||
if (filter.Categories is not null && filter.Categories.Length > 0) | ||
{ | ||
posts = posts.Where( | ||
user => user.Categories.Any(category => filter.Categories.Contains(category.Name))); | ||
} | ||
|
||
return posts; | ||
} | ||
|
||
internal static IQueryable<Post> ApplyContentFilter(string? filter, IQueryable<Post> posts) | ||
{ | ||
if (!string.IsNullOrWhiteSpace(filter)) | ||
{ | ||
posts = posts.Where(post => EF.Functions.Like(post.Text, $"%{filter}%")); | ||
} | ||
|
||
return posts; | ||
} | ||
|
||
private static ReadOnlySpan<KeyValuePair<string, object?>> MakeTagList(PostSearchFilter filter) | ||
{ | ||
return new KeyValuePair<string, object?>[] | ||
{ | ||
new(nameof(filter.Location), filter.Location), | ||
new(nameof(filter.Categories), string.Join(',', filter.Categories ?? [])), | ||
new(nameof(filter.WithinRadiusKilometers), filter.WithinRadiusKilometers) | ||
}; | ||
} | ||
} |
Oops, something went wrong.