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

Star rating system #1

Merged
merged 12 commits into from
Jan 24, 2025
1 change: 0 additions & 1 deletion src/Concertify.API/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.IdentityModel.Tokens;

namespace Concertify.API.Controllers;
Expand Down
38 changes: 36 additions & 2 deletions src/Concertify.API/Controllers/ConcertController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using Concertify.Domain.Dtos.Concert;
using System.Security.Claims;

using Concertify.Domain.Dtos.Concert;
using Concertify.Domain.Interfaces;

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Concertify.API.Controllers;
Expand All @@ -27,7 +31,10 @@ public async Task<IActionResult> GetConcertsAsync([FromQuery] ConcertFilterDto c
[Produces(typeof(ConcertDetailsDto))]
public async Task<IActionResult> GetConcertByIdAsync(int id)
{
ConcertDetailsDto concert = await _concertService.GetConcertByIdAsync(id);
string? userId = User.FindFirstValue(ClaimTypes.NameIdentifier)
?? null;

ConcertDetailsDto concert = await _concertService.GetConcertByIdAsync(id, userId);
concert.CoverImage = $"{Request.Scheme}://{Request.Host}{concert.CoverImage.Replace(_webHostEnvironment.WebRootPath, "")}";

return Ok(concert);
Expand All @@ -43,4 +50,31 @@ public async Task<IActionResult> SearchAsync([FromQuery] ConcertSearchDto concer

return Ok(concerts);
}

[HttpPost]
[Route("{id}/rate")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> RateConcertAsync(float stars, int id)
{
string userId = User.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new Exception("User Id cannot be null.");

ConcertRatingDto concertRating = new()
{
UserId = userId,
ConcertId = id,
Rating = stars
};
await _concertService.RateConcertAsync(concertRating);
return Ok();
}

[HttpGet]
[Route("{id}/average_rating")]
[Produces(typeof(double))]
public async Task<IActionResult> GetAverageRatingAsync(int id)
{
float averageRating = await _concertService.GetAverageRatingAsync(id);
return Ok(new {AverageRating = averageRating});
}
}
10 changes: 8 additions & 2 deletions src/Concertify.API/Controllers/ScrapeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ public class ScrapeController(IScraperService scraperService, IWebHostEnvironmen
[HttpGet]
[Route("collect")]
[Produces(typeof(List<ConcertSummaryDto>))]
public async IAsyncEnumerable<ConcertSummaryDto> Collect()
public async Task<IActionResult> Collect()
{
List<ConcertSummaryDto> concerts = new();
await foreach (var concert in _scraperService.Collect())
{
concert.CardImage = $"{Request.Scheme}://{Request.Host}{concert.CardImage.Replace(_webHostEnvironment.WebRootPath, "")}";
yield return concert;
concerts.Add(concert);
}

return Ok(new {
count = concerts.Count,
scrapedItems = concerts
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public static void RegisterServices(IServiceCollection services)
services.AddScoped<IEmailSender, EmailSender>();
services.AddScoped<IConcertService, ConcertService>();
services.AddScoped<IGenericRepository<Concert>, GenericRepository<Concert>>();

services.AddScoped<IGenericRepository<Rating>, GenericRepository<Rating>>();

services.AddScoped<IScraperService, ScraperService>();
services.AddScoped<IWebScraper, HonarTicketScraper>();
services.AddScoped<IScraperManager, ScraperManager>();
Expand Down
58 changes: 54 additions & 4 deletions src/Concertify.Application/Services/ConcertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,25 @@

namespace Concertify.Application.Services;

public class ConcertService(IGenericRepository<Concert> concertRepository, IMapper mapper) : IConcertService
public class ConcertService(IGenericRepository<Concert> concertRepository, IGenericRepository<Rating> ratingRepository, IMapper mapper) : IConcertService
{
private readonly IGenericRepository<Concert> _concertRepository = concertRepository;
private readonly IGenericRepository<Rating> _ratingRepository = ratingRepository;
private readonly IMapper _mapper = mapper;

public async Task<ConcertDetailsDto> GetConcertByIdAsync(int concertId)
public async Task<ConcertDetailsDto> GetConcertByIdAsync(int concertId, string? userId)
{
Concert entity = await _concertRepository.GetByIdAsync(concertId)

Concert entity = await _concertRepository.GetByIdAsync(concertId, c => c.Ratings)
?? throw new ItemNotFoundException(concertId);


ConcertDetailsDto concert = _mapper.Map<ConcertDetailsDto>(entity);
if (userId != null && entity.Ratings.Any(r => r.Id == userId))
{
concert.UserRating = (await _ratingRepository.GetFilteredAsync([r => r.UserId == userId, r => r.ConcertId == concertId], null, null)).First().Stars;

}
return concert;

}
Expand All @@ -40,7 +47,9 @@ public async Task<ConcertListDto> GetConcertsAsync(ConcertFilterDto concertFilte
c => concertFilterDto.TicketPriceRangeEnd == null || c.TicketPrice.Contains(concertFilterDto.TicketPriceRangeEnd.Value)
];

List<Concert> concerts = await _concertRepository.GetFilteredAsync(filters, concertFilterDto.Skip, concertFilterDto.Take);
Expression<Func<Concert, object>>[] includes = [c => c.Ratings];

List<Concert> concerts = await _concertRepository.GetFilteredAsync(filters, concertFilterDto.Skip, concertFilterDto.Take, includes);

int totalCount = concerts.Count;

Expand Down Expand Up @@ -70,4 +79,45 @@ public async Task<List<ConcertSummaryDto>> SearchAsync(ConcertSearchDto concertS

return concertDtos;
}

public async Task RateConcertAsync(ConcertRatingDto concertRating)
{
Concert concert = await _concertRepository.GetByIdAsync(concertRating.ConcertId)
?? throw new ItemNotFoundException(concertRating.ConcertId);

Rating? rating = (await _ratingRepository.GetFilteredAsync([
r => r.ConcertId == concertRating.ConcertId
&& r.UserId == concertRating.UserId], null, null))
.FirstOrDefault();


if (rating == null)
{

Rating newRating = new()
{
ConcertId = concertRating.ConcertId,
Stars = concertRating.Rating,
UserId = concertRating.UserId
};
await _ratingRepository.InsertAsync(newRating);
}
else
{
rating.Stars = concertRating.Rating;
_ratingRepository.Update(rating);
}

await _ratingRepository.SaveChangesAsync();

concert.AverageRating = await GetAverageRatingAsync(concertRating.ConcertId);
_concertRepository.Update(concert);
await _concertRepository.SaveChangesAsync();
}

public async Task<float> GetAverageRatingAsync(int concertId)
{
float averageRating = (await _ratingRepository.GetFilteredAsync([r => r.ConcertId == concertId], null, null)).Average(r => r.Stars);
return averageRating;
}
}
2 changes: 2 additions & 0 deletions src/Concertify.Domain/Dtos/Concert/ConcertDetailsDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public record ConcertDetailsDto
public float Longtitude { get; set; } = default!;
public string CoverImage { get; set; } = default!;
public string Url { get; set; } = default!;
public float AverageRating { get; set; } = default!;
public float? UserRating { get; set; }
}
8 changes: 8 additions & 0 deletions src/Concertify.Domain/Dtos/Concert/ConcertRatingDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Concertify.Domain.Dtos.Concert;

public record ConcertRatingDto
{
public string UserId { get; set; }
public int ConcertId { get; set; }
public float Rating { get; set; }
}
1 change: 1 addition & 0 deletions src/Concertify.Domain/Dtos/Concert/ConcertSummaryDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public record ConcertSummaryDto
public string Location { get; set; } = default!;
public string Category { get; set; } = default!;
public string CardImage { get; set; } = default!;
public float AverageRating { get; set; } = default!;

}
4 changes: 3 additions & 1 deletion src/Concertify.Domain/Interfaces/IConcertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ namespace Concertify.Domain.Interfaces;
public interface IConcertService
{
public Task<ConcertListDto> GetConcertsAsync(ConcertFilterDto concertFilterDto);
public Task<ConcertDetailsDto> GetConcertByIdAsync(int concertId);
public Task<ConcertDetailsDto> GetConcertByIdAsync(int concertId, string? userId);
public Task<List<ConcertSummaryDto>> SearchAsync(ConcertSearchDto concertSearch);
public Task RateConcertAsync(ConcertRatingDto concertRating);
public Task<float> GetAverageRatingAsync(int concertId);

}
6 changes: 3 additions & 3 deletions src/Concertify.Domain/Interfaces/IGenericRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ public interface IGenericRepository<T> where T : EntityBase
public Task<List<T>> GetFilteredAsync(Expression<Func<T, bool>>[] filtered,
int? skip,
int? take,
params Expression<Func<T, bool>>[] includes);
params Expression<Func<T, object>>[] includes);

public Task<List<T>> GetAsync(int? skip, int? take, params Expression<Func<T, bool>>[] includes);
public Task<T> GetByIdAsync(int id, params Expression<Func<T, bool>>[] includes);
public Task<List<T>> GetAsync(int? skip, int? take, params Expression<Func<T, object>>[] includes);
public Task<T> GetByIdAsync(int id, params Expression<Func<T, object>>[] includes);
public Task<int> InsertAsync (T entity);
public void Update (T entity);
public void Delete(T entity);
Expand Down
4 changes: 2 additions & 2 deletions src/Concertify.Domain/Models/ApplicationUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace Concertify.Domain.Models;

public class ApplicationUser : IdentityUser
{
//private string _username = default!;
[ProtectedPersonalData]
//public new string UserName { get => _username; set => _username = value; }
public override string? UserName { get; set; } = default!;
public string? FirstName { get; set; }
public string? LastName { get; set; }

public List<Concert> RatedConcerts { get; set; } = [];
}
2 changes: 2 additions & 0 deletions src/Concertify.Domain/Models/Concert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public class Concert : EntityBase
public string CardImage { get; set; } = default!;
public string Url { get; set; } = default!;

public List<ApplicationUser> Ratings { get; set; } = [];
public float AverageRating { get; set; } = default!;
}
9 changes: 9 additions & 0 deletions src/Concertify.Domain/Models/Rating.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace Concertify.Domain.Models;

public class Rating : EntityBase
{
public string? UserId { get; set; }
public int ConcertId { get; set; }
public float Stars { get; set; }
}
23 changes: 22 additions & 1 deletion src/Concertify.Infrastructure/Data/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Concertify.Infrastructure.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public DbSet<Concert> Concerts { get; set; }
public DbSet<Rating> Ratings { get; set; }

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
Expand All @@ -27,12 +28,32 @@ protected override void OnModelCreating(ModelBuilder builder)
base.OnModelCreating(builder);

builder.Entity<Concert>().HasKey(e => e.Id);
builder.Entity<Rating>().HasKey(e => e.Id);

builder.Entity<Rating>()
.HasIndex(r => new {r.UserId, r.ConcertId })
.IsUnique();

builder.Entity<Concert>()
.HasIndex(c => new
{
c.Title,
c.StartDateTime,
c.City,
}).IsUnique();

builder.Entity<Concert>()
.HasMany(e => e.Ratings)
.WithMany(e => e.RatedConcerts)
.UsingEntity<Rating>(
l => l.HasOne<ApplicationUser>().WithMany().HasForeignKey(e => e.UserId),
r => r.HasOne<Concert>().WithMany().HasForeignKey(e => e.ConcertId));

builder.Entity<Concert>()
.Property(c => c.StartDateTime)
.HasConversion(
src => src.Kind == DateTimeKind.Utc ? src : DateTime.SpecifyKind(src, DateTimeKind.Utc),
dest => dest.Kind == DateTimeKind.Utc ? dest : DateTime.SpecifyKind(dest, DateTimeKind.Utc)
);

}
}
6 changes: 3 additions & 3 deletions src/Concertify.Infrastructure/Data/GenericRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public GenericRepository(ApplicationDbContext context)
_dbSet = context.Set<T>();
}

public async Task<List<T>> GetFilteredAsync(Expression<Func<T, bool>>[] filters, int? skip, int? take, params Expression<Func<T, bool>>[] includes)
public async Task<List<T>> GetFilteredAsync(Expression<Func<T, bool>>[] filters, int? skip, int? take, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = _dbSet.AsQueryable<T>();

Expand All @@ -40,7 +40,7 @@ public async Task<List<T>> GetFilteredAsync(Expression<Func<T, bool>>[] filters,

}

public async Task<List<T>> GetAsync(int? skip, int? take, params Expression<Func<T, bool>>[] includes)
public async Task<List<T>> GetAsync(int? skip, int? take, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = _dbSet.AsQueryable<T>();

Expand All @@ -52,7 +52,7 @@ public async Task<List<T>> GetAsync(int? skip, int? take, params Expression<Func
return await query.ToListAsync();
}

public async Task<T> GetByIdAsync(int id, params Expression<Func<T, bool>>[] includes)
public async Task<T> GetByIdAsync(int id, params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = _dbSet.AsQueryable<T>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
using Concertify.Infrastructure.Data;
using Concertify.Infrastructure.Interfaces;

using Microsoft.EntityFrameworkCore;

namespace Concertify.Infrastructure.ExternalServices.Scrapers;

public class ScraperManager(IWebScraper scraper, ApplicationDbContext context) : IScraperManager
Expand All @@ -14,6 +16,9 @@ public async IAsyncEnumerable<Concert> StartScraping(string url)
{
await foreach (var concert in _scraper.ExtractLinks(url))
{
bool exists = await _context.Concerts.AnyAsync(c => c.Title == concert.Title && c.StartDateTime == concert.StartDateTime && c.City == concert.City);
if (exists)
continue;
await _context.Concerts.AddAsync(concert);
await _context.SaveChangesAsync();
yield return concert;
Expand Down
Loading