Skip to content
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

Feature/posts #421

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions src/shared/Jordnaer.Shared/Database/GroupCategory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Jordnaer.Shared;

public class GroupCategory
{
public required Guid GroupId { get; set; }
public required Guid GroupId { get; set; }

public required int CategoryId { get; set; }
public required int CategoryId { get; set; }
}
30 changes: 30 additions & 0 deletions src/shared/Jordnaer.Shared/Database/GroupPost.cs
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!;
}
27 changes: 27 additions & 0 deletions src/shared/Jordnaer.Shared/Database/Post.cs
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; } = [];
}
8 changes: 8 additions & 0 deletions src/shared/Jordnaer.Shared/Database/PostCategory.cs
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; }
}
24 changes: 24 additions & 0 deletions src/shared/Jordnaer.Shared/Extensions/PostExtensions.cs
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()
};
}
}
21 changes: 21 additions & 0 deletions src/shared/Jordnaer.Shared/Posts/PostDto.cs
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; } = [];
}
57 changes: 57 additions & 0 deletions src/shared/Jordnaer.Shared/Posts/PostSearchFilter.cs
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!;
}
}
NielsPilgaard marked this conversation as resolved.
Show resolved Hide resolved

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!;
}
}
NielsPilgaard marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions src/shared/Jordnaer.Shared/Posts/PostSearchResult.cs
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; }
}
22 changes: 21 additions & 1 deletion src/web/Jordnaer/Database/JordnaerDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class JordnaerDbContext : IdentityDbContext<ApplicationUser>
public DbSet<Category> Categories { get; set; } = default!;
public DbSet<UserProfileCategory> UserProfileCategories { get; set; } = default!;
public DbSet<UserContact> UserContacts { get; set; } = default!;
public DbSet<Shared.Chat> Chats { get; set; } = default!;
public DbSet<Chat> Chats { get; set; } = default!;
public DbSet<ChatMessage> ChatMessages { get; set; } = default!;
public DbSet<UnreadMessage> UnreadMessages { get; set; } = default;
public DbSet<UserChat> UserChats { get; set; } = default!;
Expand All @@ -20,8 +20,28 @@ public class JordnaerDbContext : IdentityDbContext<ApplicationUser>
public DbSet<GroupMembership> GroupMemberships { get; set; } = default!;
public DbSet<GroupCategory> GroupCategories { get; set; } = default!;

public DbSet<Post> Posts { get; set; } = default!;
public DbSet<GroupPost> GroupPosts { get; set; } = default!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(e => e.UserProfile)
.WithMany();

modelBuilder.Entity<Post>()
.HasMany(e => e.Categories)
.WithMany()
.UsingEntity<PostCategory>();

modelBuilder.Entity<GroupPost>()
.HasOne(e => e.UserProfile)
.WithMany();

modelBuilder.Entity<GroupPost>()
.HasOne(e => e.Group)
.WithMany();

modelBuilder.Entity<Group>()
.HasMany(e => e.Members)
.WithMany(e => e.Groups)
Expand Down
76 changes: 76 additions & 0 deletions src/web/Jordnaer/Features/GroupPosts/PostService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Jordnaer.Database;
using Jordnaer.Shared;
using Microsoft.EntityFrameworkCore;
using OneOf;
using OneOf.Types;

namespace Jordnaer.Features.GroupPosts;

public interface IGroupPostService
{
Task<OneOf<PostDto, NotFound>> GetPostAsync(Guid postId,
CancellationToken cancellationToken = default);

Task<OneOf<Success, Error<string>>> CreatePostAsync(GroupPost post,
CancellationToken cancellationToken = default);

Task<OneOf<Success, Error<string>>> DeletePostAsync(Guid postId,
CancellationToken cancellationToken = default);
}
// TODO: This is just a copy of PostService, make it Group specific
public class GroupPostService(IDbContextFactory<JordnaerDbContext> contextFactory) : IGroupPostService
NielsPilgaard marked this conversation as resolved.
Show resolved Hide resolved
{
public async Task<OneOf<PostDto, NotFound>> GetPostAsync(Guid postId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

var post = await context.Posts
.Where(x => x.Id == postId)
.Select(x => x.ToPostDto())
.FirstOrDefaultAsync(cancellationToken);

return post is null
? new NotFound()
: post;
}

public async Task<OneOf<Success, Error<string>>> CreatePostAsync(GroupPost post,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

if (await context.Posts
.AsNoTracking()
.AnyAsync(x => x.Id == post.Id &&
x.UserProfileId == post.UserProfileId,
cancellationToken))
{
return new Error<string>("Opslaget eksisterer allerede.");
}

context.GroupPosts.Add(post);

await context.SaveChangesAsync(cancellationToken);

return new Success();
}

public async Task<OneOf<Success, Error<string>>> DeletePostAsync(Guid postId,
CancellationToken cancellationToken = default)
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

var post = await context.Posts.FindAsync([postId], cancellationToken);
if (post is null)
{
return new Error<string>("Opslaget blev ikke fundet.");
}

await context.Posts
.Where(x => x.Id == postId)
.ExecuteDeleteAsync(cancellationToken);

return new Success();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public async Task<GroupSearchResult> GetGroupsAsync(GroupSearchFilter filter,
groups = groups.OrderBy(user => user.CreatedUtc);
}

// TODO: Try-catch and error in return type
NielsPilgaard marked this conversation as resolved.
Show resolved Hide resolved
var groupsToSkip = filter.PageNumber == 1 ? 0 : (filter.PageNumber - 1) * filter.PageSize;
var paginatedGroups = await groups
.Skip(groupsToSkip)
Expand Down
5 changes: 5 additions & 0 deletions src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ internal static class JordnaerMetrics
internal static readonly Counter<int> UserSearchesCounter =
Meter.CreateCounter<int>("jordnaer_user_user_searches_total");

internal static readonly Counter<int> PostSearchesCounter =
Meter.CreateCounter<int>("jordnaer_post_post_searches_total");
internal static readonly Counter<int> PostsCreatedCounter =
Meter.CreateCounter<int>("jordnaer_post_posts_created_total");

internal static readonly Counter<int> SponsorAdViewCounter =
Meter.CreateCounter<int>("jordnaer_ad_sponsor_views_total");
}
Loading