Skip to content

Commit

Permalink
Merge pull request #28 from davewalker5/MC-150-Genre-Based-Browsing
Browse files Browse the repository at this point in the history
MC-150 Genre based catalogue browsing
  • Loading branch information
davewalker5 authored Nov 18, 2023
2 parents f51736a + 43c4caf commit 4d0ce02
Show file tree
Hide file tree
Showing 37 changed files with 432 additions and 144 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ MusicCatalogue.LookupTool --lookup "John Coltrane" "Blue Train" catalogue

- Clicking on the artist name in any row in the track list or clicking on the "Back" button returns to the album list for that artist

#### Browsing By Genre

- To browse by genre, click on the "Genres" menu item
- A page listing the genres derived from all albums in the //main catalogue// is displayed

<img src="diagrams/genre-list.png" alt="Genre List" width="600">

- As the mouse pointer is moved up and down the table, the current row is highlighted
- Clicking on a row opens the artist list for the genre shown in that row

#### The Wish List

- To view the wish list, click on the "Wish List" menu item
Expand Down
Binary file modified diagrams/artist-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added diagrams/genre-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docker/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/core/aspnet:latest
COPY musiccatalogue.api-1.17.0.0 /opt/musiccatalogue.api-1.17.0.0
WORKDIR /opt/musiccatalogue.api-1.17.0.0/bin
COPY musiccatalogue.api-1.18.0.0 /opt/musiccatalogue.api-1.18.0.0
WORKDIR /opt/musiccatalogue.api-1.18.0.0/bin
ENTRYPOINT [ "./MusicCatalogue.Api" ]
4 changes: 2 additions & 2 deletions docker/ui/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM node:20-alpine
COPY musiccatalogue.ui-1.17.0.0 /opt/musiccatalogue.ui-1.17.0.0
WORKDIR /opt/musiccatalogue.ui-1.17.0.0
COPY musiccatalogue.ui-1.18.0.0 /opt/musiccatalogue.ui-1.18.0.0
WORKDIR /opt/musiccatalogue.ui-1.18.0.0
RUN npm install
RUN npm run build
ENTRYPOINT [ "npm", "start" ]
19 changes: 12 additions & 7 deletions src/MusicCatalogue.Api/Controllers/GenresController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using MusicCatalogue.Entities.Database;
using MusicCatalogue.Entities.Exceptions;
using MusicCatalogue.Entities.Interfaces;
using MusicCatalogue.Entities.Search;

namespace MusicCatalogue.Api.Controllers
{
Expand All @@ -20,18 +21,22 @@ public GenresController(IMusicCatalogueFactory factory)
}

/// <summary>
/// Return a list of all the genres in the catalogue
/// Return a list of genres matching the specified criteria
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("")]
public async Task<ActionResult<List<Genre>>> GetGenresAsync()
[HttpPost]
[Route("search")]
public async Task<ActionResult<List<Genre>>> GetGenresAsync(GenreSearchCriteria criteria)
{
// Get a list of all artists in the catalogue
List<Genre> genres = await _factory.Genres.ListAsync(x => true);
// Ideally, this method would use the GET verb but as more filtering criteria are added that leads
// to an increasing number of query string parameters and a very messy URL. So the filter criteria
// are POSTed in the request body, instead, and bound into a strongly typed criteria object

// Retrieve a list of matching genres
var genres = await _factory.Search.GenreSearchAsync(criteria);

// If there are no genres, return a no content response
if (!genres.Any())
if (genres == null)
{
return NoContent();
}
Expand Down
6 changes: 3 additions & 3 deletions src/MusicCatalogue.Api/MusicCatalogue.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ReleaseVersion>1.17.0.0</ReleaseVersion>
<FileVersion>1.17.0.0</FileVersion>
<ProductVersion>1.17.0</ProductVersion>
<ReleaseVersion>1.18.0.0</ReleaseVersion>
<FileVersion>1.18.0.0</FileVersion>
<ProductVersion>1.18.0</ProductVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/MusicCatalogue.Data/MusicCatalogue.Data.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>MusicCatalogue.Data</PackageId>
<PackageVersion>1.16.0.0</PackageVersion>
<PackageVersion>1.17.0.0</PackageVersion>
<Authors>Dave Walker</Authors>
<Copyright>Copyright (c) Dave Walker 2023</Copyright>
<Owners>Dave Walker</Owners>
Expand All @@ -17,7 +17,7 @@
<PackageProjectUrl>https://github.com/davewalker5/MusicCatalogue</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<ReleaseVersion>1.16.0.0</ReleaseVersion>
<ReleaseVersion>1.17.0.0</ReleaseVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/MusicCatalogue.Entities/Interfaces/ISearchManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public interface ISearchManager
{
Task<List<Album>?> AlbumSearchAsync(AlbumSearchCriteria criteria);
Task<List<Artist>?> ArtistSearchAsync(ArtistSearchCriteria criteria);
Task<List<Genre>?> GenreSearchAsync(GenreSearchCriteria criteria);
}
}
4 changes: 2 additions & 2 deletions src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>MusicCatalogue.Entities</PackageId>
<PackageVersion>1.16.0.0</PackageVersion>
<PackageVersion>1.17.0.0</PackageVersion>
<Authors>Dave Walker</Authors>
<Copyright>Copyright (c) Dave Walker 2023</Copyright>
<Owners>Dave Walker</Owners>
Expand All @@ -17,7 +17,7 @@
<PackageProjectUrl>https://github.com/davewalker5/MusicCatalogue</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<ReleaseVersion>1.16.0.0</ReleaseVersion>
<ReleaseVersion>1.17.0.0</ReleaseVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
10 changes: 10 additions & 0 deletions src/MusicCatalogue.Entities/Search/GenreSearchCriteria.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Diagnostics.CodeAnalysis;

namespace MusicCatalogue.Entities.Search
{
[ExcludeFromCodeCoverage]
public class GenreSearchCriteria
{
public bool? WishList { get; set; }
}
}
48 changes: 37 additions & 11 deletions src/MusicCatalogue.Logic/Database/SearchManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using MusicCatalogue.Entities.Database;
using MusicCatalogue.Entities.Interfaces;
using MusicCatalogue.Entities.Search;
using System.Runtime.InteropServices;

namespace MusicCatalogue.Logic.Database
{
Expand Down Expand Up @@ -30,29 +29,29 @@ internal SearchManager(IMusicCatalogueFactory factory) : base(factory)
// as "return artists who have produced an album in the Jazz genre". So, start by retrieving a
// list of albums matching the criteria then derive the artists from that
var albums = await Factory.Albums
.ListAsync(x => (
.ListAsync(x => (
(criteria.WishList == null) ||
((criteria.WishList == false) && (x.IsWishListItem == null)) ||
(x.IsWishListItem == criteria.WishList)
) &&
(
) &&
(
(criteria.GenreId == null) ||
(x.GenreId == criteria.GenreId)
));
));

// If there are no albums, there can't be any matching artists
if (albums.Any())
{
// Compile a list of artist IDs and load the matching artists
var artistIds = albums.Select(x => x.ArtistId).ToList();
// Compile a list of unique artist IDs and load the matching artists
var artistIds = albums.Select(x => x.ArtistId).Distinct().ToList();
artists = await Factory.Artists
.ListAsync(x => artistIds.Contains(x.Id) &&
(
.ListAsync(x => artistIds.Contains(x.Id) &&
(
(prefix == null) ||
((x.SearchableName != null) && x.SearchableName.StartsWith(prefix)) ||
((x.SearchableName == null) && x.Name.StartsWith(prefix))
),
false);
),
false);

// Now map the albums onto their associated artists
foreach (var artist in artists)
Expand Down Expand Up @@ -90,5 +89,32 @@ internal SearchManager(IMusicCatalogueFactory factory) : base(factory)
return albums.Any() ? albums : null;
}

/// <summary>
/// Return the genres matching the specified criteria
/// </summary>
/// <param name="criteria"></param>
/// <returns></returns>
public async Task<List<Genre>?> GenreSearchAsync(GenreSearchCriteria criteria)
{
List<Genre>? genres = null;

// Retrieve a list of albums, as they're the entity that's tagged with a genre
var albums = await Factory.Albums
.ListAsync(x => (
(criteria.WishList == null) ||
((criteria.WishList == false) && (x.IsWishListItem == null)) ||
(x.IsWishListItem == criteria.WishList)
));

// If there are no albums, there can't be any matching artists
if (albums.Any())
{
// Get a list of unique genre IDs and load the matching genres
var genreIds = albums.Select(x => x.GenreId).Distinct().ToList();
genres = await Factory.Genres.ListAsync(x => genreIds.Contains(x.Id));
}

return genres;
}
}
}
4 changes: 2 additions & 2 deletions src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageId>MusicCatalogue.Logic</PackageId>
<PackageVersion>1.16.0.0</PackageVersion>
<PackageVersion>1.17.0.0</PackageVersion>
<Authors>Dave Walker</Authors>
<Copyright>Copyright (c) Dave Walker 2023</Copyright>
<Owners>Dave Walker</Owners>
Expand All @@ -17,7 +17,7 @@
<PackageProjectUrl>https://github.com/davewalker5/MusicCatalogue</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<ReleaseVersion>1.16.0.0</ReleaseVersion>
<ReleaseVersion>1.17.0.0</ReleaseVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ReleaseVersion>1.16.0.0</ReleaseVersion>
<FileVersion>1.16.0.0</FileVersion>
<ProductVersion>1.16.0</ProductVersion>
<ReleaseVersion>1.17.0.0</ReleaseVersion>
<FileVersion>1.17.0.0</FileVersion>
<ProductVersion>1.17.0</ProductVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
2 changes: 1 addition & 1 deletion src/MusicCatalogue.Tests/AlbumSearchManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void TestInitialize()

// Add the artists
_jazzArtistId = Task.Run(() => _factory.Artists.AddAsync("Diana Krall")).Result.Id;
_popGenreId = Task.Run(() => _factory.Artists.AddAsync("Katie Melua")).Result.Id;
_popArtistId = Task.Run(() => _factory.Artists.AddAsync("Katie Melua")).Result.Id;

// Add the albums, one on the wishlist and one not
Task.Run(() => _factory.Albums.AddAsync(_jazzArtistId, _jazzGenreId, JazzAlbumTitle, 2002, null, false, null, null, null)).Wait();
Expand Down
82 changes: 82 additions & 0 deletions src/MusicCatalogue.Tests/GenreSearchManagerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using MusicCatalogue.Data;
using MusicCatalogue.Entities.Interfaces;
using MusicCatalogue.Entities.Search;
using MusicCatalogue.Logic.Factory;

namespace MusicCatalogue.Tests
{
[TestClass]
public class GenreSearchManagerTest
{
private const string JazzGenre = "Jazz";
private const string PopGenre = "Pop";

private IMusicCatalogueFactory? _factory;
private int _jazzGenreId;
private int _popGenreId;
private int _jazzArtistId;
private int _popArtistId;

[TestInitialize]
public void TestInitialize()
{
MusicCatalogueDbContext context = MusicCatalogueDbContextFactory.CreateInMemoryDbContext();
_factory = new MusicCatalogueFactory(context);

// Add the genres
_jazzGenreId = Task.Run(() => _factory.Genres.AddAsync("Jazz")).Result.Id;
_popGenreId = Task.Run(() => _factory.Genres.AddAsync("Pop")).Result.Id;

// Add the artists
_jazzArtistId = Task.Run(() => _factory.Artists.AddAsync("Diana Krall")).Result.Id;
_popArtistId = Task.Run(() => _factory.Artists.AddAsync("Katie Melua")).Result.Id;
}

[TestMethod]
public async Task SearchForAllGenresTest()
{
// Add the albums, one on the wishlist and one not
Task.Run(() => _factory!.Albums.AddAsync(_jazzArtistId, _jazzGenreId, "Live In Paris", 2002, null, false, null, null, null)).Wait();
Task.Run(() => _factory!.Albums.AddAsync(_popArtistId, _popGenreId, "Album No. 8", 2020, null, true, null, null, null)).Wait();

var genres = await _factory!.Search.GenreSearchAsync(new GenreSearchCriteria());
Assert.IsNotNull(genres);
Assert.AreEqual(2, genres.Count);
}

[TestMethod]
public async Task SearchMainCatalogueGenresTest()
{
// Add the albums, one on the wishlist and one not
Task.Run(() => _factory!.Albums.AddAsync(_jazzArtistId, _jazzGenreId, "Live In Paris", 2002, null, false, null, null, null)).Wait();
Task.Run(() => _factory!.Albums.AddAsync(_popArtistId, _popGenreId, "Album No. 8", 2020, null, true, null, null, null)).Wait();

var criteria = new GenreSearchCriteria { WishList = false };
var genres = await _factory!.Search.GenreSearchAsync(criteria);
Assert.IsNotNull(genres);
Assert.AreEqual(1, genres.Count);
Assert.AreEqual(JazzGenre, genres[0].Name);
}

[TestMethod]
public async Task SearchWishlistGenresTest()
{
// Add the albums, one on the wishlist and one not
Task.Run(() => _factory!.Albums.AddAsync(_jazzArtistId, _jazzGenreId, "Live In Paris", 2002, null, false, null, null, null)).Wait();
Task.Run(() => _factory!.Albums.AddAsync(_popArtistId, _popGenreId, "Album No. 8", 2020, null, true, null, null, null)).Wait();

var criteria = new GenreSearchCriteria { WishList = true };
var genres = await _factory!.Search.GenreSearchAsync(criteria);
Assert.IsNotNull(genres);
Assert.AreEqual(1, genres.Count);
Assert.AreEqual(PopGenre, genres[0].Name);
}

[TestMethod]
public async Task NoMatchesTest()
{
var genres = await _factory!.Search.GenreSearchAsync(new GenreSearchCriteria());
Assert.IsNull(genres);
}
}
}
19 changes: 12 additions & 7 deletions src/music-catalogue-ui/components/albumPurchaseDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
// If the returned album is valid, navigate back to the albums by artist page.
// Otherwise, show an error
if (updatedAlbum != null) {
navigate(
pages.albums,
artist,
updatedAlbum,
updatedAlbum.isWishListItem
);
navigate({
page: pages.albums,
artist: artist,
album: updatedAlbum,
isWishList: updatedAlbum.isWishListItem,
});
} else {
setErrorMessage("Error updating the album purchase details");
}
Expand Down Expand Up @@ -151,7 +151,12 @@ const AlbumPurchaseDetails = ({ artist, album, navigate, logout }) => {
<button
className="btn btn-primary"
onClick={() =>
navigate(pages.albums, artist, album, album.isWishListItem)
navigate({
page: pages.albums,
artist: artist,
album: album,
isWishList: album.isWishListItem,
})
}
>
Cancel
Expand Down
Loading

0 comments on commit 4d0ce02

Please sign in to comment.