diff --git a/RMuseum/Controllers/GanjoorController.cs b/RMuseum/Controllers/GanjoorController.cs index f9018351..7af9bfb0 100644 --- a/RMuseum/Controllers/GanjoorController.cs +++ b/RMuseum/Controllers/GanjoorController.cs @@ -59,6 +59,50 @@ namespace RMuseum.Controllers return Ok(poets); } + /// + /// get list of books (GanjoorCat entries whose CatType is Book, sorted alphabetically by name), + /// optionally filtered by (part of) name and/or poet id. Not to be confused with + /// GetBooksAsync()/"books" above, which lists GanjoorCat entries by their (separate, + /// legacy) BookName field for cover-image generation. + /// + /// optional, only books whose name contains this + /// optional, only books belonging to this poet + /// + [HttpGet] + [Route("book-catalog")] + [AllowAnonymous] + [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorBookViewModel[]))] + [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] + public async Task GetBookCatalog(string name = null, int? poetId = null) + { + // only the common, unfiltered call (the one the home page's book search will use to load + // the full list once and filter client-side) is worth caching - a filtered call is cheap + // on a table this small (well under 200 rows) and caching every name/poetId combination + // would just grow the cache for no benefit. + if (string.IsNullOrEmpty(name) && poetId == null) + { + var cacheKey = $"ganjoor/book-catalog"; + if (!_memoryCache.TryGetValue(cacheKey, out GanjoorBookViewModel[] cachedBooks)) + { + RServiceResult cachedRes = + await _ganjoorService.GetBookCatalogAsync(); + if (!string.IsNullOrEmpty(cachedRes.ExceptionString)) + return BadRequest(cachedRes.ExceptionString); + + cachedBooks = cachedRes.Result; + if (AggressiveCacheEnabled) + _memoryCache.Set(cacheKey, cachedBooks, TimeSpan.FromHours(1)); + } + return Ok(cachedBooks); + } + + RServiceResult res = + await _ganjoorService.GetBookCatalogAsync(name, poetId); + if (!string.IsNullOrEmpty(res.ExceptionString)) + return BadRequest(res.ExceptionString); + return Ok(res.Result); + } + /// /// gets list of poets grouped by centuries (first one is the pinned ones) /// diff --git a/RMuseum/Models/Ganjoor/ViewModels/GanjoorBookViewModel.cs b/RMuseum/Models/Ganjoor/ViewModels/GanjoorBookViewModel.cs new file mode 100644 index 00000000..7504eaa9 --- /dev/null +++ b/RMuseum/Models/Ganjoor/ViewModels/GanjoorBookViewModel.cs @@ -0,0 +1,39 @@ +namespace RMuseum.Models.Ganjoor.ViewModels +{ + /// + /// A GanjoorCat whose CatType is Book - a minimal view model for listing/searching books + /// (e.g. the home page's book search box), not the full category details + /// + public class GanjoorBookViewModel + { + /// + /// id (GanjoorCat.Id) + /// + public int Id { get; set; } + + /// + /// book name (GanjoorCat.Title) + /// + public string Name { get; set; } + + /// + /// full url, sample: /hafez/ghazal + /// + public string FullUrl { get; set; } + + /// + /// poet id (GanjoorCat.PoetId) + /// + public int PoetId { get; set; } + + /// + /// poet short name (GanjoorPoet.Nickname) + /// + public string PoetName { get; set; } + + public override string ToString() + { + return Name; + } + } +} diff --git a/RMuseum/RMuseum.xml b/RMuseum/RMuseum.xml index 6d6ea177..f484ad83 100644 --- a/RMuseum/RMuseum.xml +++ b/RMuseum/RMuseum.xml @@ -1096,6 +1096,17 @@ + + + get list of books (GanjoorCat entries whose CatType is Book, sorted alphabetically by name), + optionally filtered by (part of) name and/or poet id. Not to be confused with + GetBooksAsync()/"books" above, which lists GanjoorCat entries by their (separate, + legacy) BookName field for cover-image generation. + + optional, only books whose name contains this + optional, only books belonging to this poet + + gets list of poets grouped by centuries (first one is the pinned ones) @@ -12578,6 +12589,37 @@ simulate naming + + + A GanjoorCat whose CatType is Book - a minimal view model for listing/searching books + (e.g. the home page's book search box), not the full category details + + + + + id (GanjoorCat.Id) + + + + + book name (GanjoorCat.Title) + + + + + full url, sample: /hafez/ghazal + + + + + poet id (GanjoorCat.PoetId) + + + + + poet short name (GanjoorPoet.Nickname) + + correct id @@ -18549,6 +18591,16 @@ + + + Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name. + Not to be confused with GetBooksAsync() below, which lists GanjoorCat entries by their + (separate, legacy) BookName field for cover-image generation. + + optional, only books whose name contains this (case-insensitive) + optional, only books belonging to this poet + + get poet by id @@ -22286,6 +22338,9 @@ IGanjoorService implementation + + IGanjoorService implementation + @@ -22366,6 +22421,16 @@ + + + Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name. + Not to be confused with GetBooksAsync() elsewhere in this class, which lists GanjoorCat + entries by their (separate, legacy) BookName field for cover-image generation. + + optional, only books whose name contains this (case-insensitive) + optional, only books belonging to this poet + + category word counts diff --git a/RMuseum/Services/IGanjoorService.cs b/RMuseum/Services/IGanjoorService.cs index 39024c18..35c8e68f 100644 --- a/RMuseum/Services/IGanjoorService.cs +++ b/RMuseum/Services/IGanjoorService.cs @@ -22,6 +22,16 @@ namespace RMuseum.Services /// Task> GetPoets(bool published, bool includeBio = true); + /// + /// Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name. + /// Not to be confused with GetBooksAsync() below, which lists GanjoorCat entries by their + /// (separate, legacy) BookName field for cover-image generation. + /// + /// optional, only books whose name contains this (case-insensitive) + /// optional, only books belonging to this poet + /// + Task> GetBookCatalogAsync(string name = null, int? poetId = null); + /// /// get poet by id /// diff --git a/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Books.cs b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Books.cs new file mode 100644 index 00000000..bbc7cb82 --- /dev/null +++ b/RMuseum/Services/Implementation/GanjoorService-Partials/GanjoorService-Books.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using RMuseum.Models.Ganjoor; +using RMuseum.Models.Ganjoor.ViewModels; +using RSecurityBackend.Models.Generic; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace RMuseum.Services.Implementation +{ + /// + /// IGanjoorService implementation + /// + public partial class GanjoorService : IGanjoorService + { + /// + /// Get list of books (GanjoorCat entries whose CatType is Book), sorted alphabetically by name. + /// Not to be confused with GetBooksAsync() elsewhere in this class, which lists GanjoorCat + /// entries by their (separate, legacy) BookName field for cover-image generation. + /// + /// optional, only books whose name contains this (case-insensitive) + /// optional, only books belonging to this poet + /// + public async Task> GetBookCatalogAsync(string name = null, int? poetId = null) + { + var query = _context.GanjoorCategories.AsNoTracking() + .Where(c => c.CatType == GanjoorCatType.Book); + + if (!string.IsNullOrWhiteSpace(name)) + { + query = query.Where(c => c.Title.Contains(name)); + } + + if (poetId != null) + { + query = query.Where(c => c.PoetId == poetId.Value); + } + + List books = + await query + .Select(c => new GanjoorBookViewModel() + { + Id = c.Id, + Name = c.Title, + FullUrl = c.FullUrl, + PoetId = c.PoetId, + PoetName = c.Poet.Nickname, + }) + .ToListAsync(); + + // the fa-IR comparer can't be translated to SQL, so sorting happens here in memory - + // the same approach GetPoets() above uses, fine for a list this small (< 200 rows) + StringComparer fa = StringComparer.Create(new CultureInfo("fa-IR"), true); + books.Sort((a, b) => fa.Compare(a.Name, b.Name)); + + return new RServiceResult(books.ToArray()); + } + } +}