Kavita/API/Controllers/CollectionController.cs
Joe Milazzo 089658e469
UX Alignment and bugfixes (#1663)
* Refactored the design of reading list page to follow more in line with list view. Added release date on the reading list items, if it's set in underlying chapter.

Fixed a bug where reordering the list items could sometimes not update correctly with drag and drop.

* Removed a bug marker that I just fixed

* When generating library covers, make them much smaller as they are only ever icons.

* Fixed library settings not showing the correct image.

* Fixed a bug where duplicate collection tags could be created.

Fixed a bug where collection tag normalized title was being set to uppercase.

Redesigned the edit collection tag modal to align with new library settings and provide inline name checks.

* Updated edit reading list modal to align with new library settings modal pattern. Refactored the backend to ensure it flows correctly without allowing duplicate names.

Don't show Continue point on series detail if the whole series is read.

* Added some more unit tests around continue point

* Fixed a bug on series detail when bulk selecting between volume and chapters, the code which determines which chapters are selected didn't take into account mixed layout for Storyline tab.

* Refactored to generate an OpenAPI spec at root of Kavita. This will be loaded by a new API site for easy hosting.

Deprecated EnableSwaggerUi preference as after validation new system works, this will be removed and instances can use our hosting to hit their server (or run a debug build).

* Test GA

* Reverted GA and instead do it in the build step. This will just force developers to commit it in.

* GA please work

* Removed redundant steps from test since build already does it.

* Try another GA

* Moved all test actions into initial build step, which should drastically cut down on time. Only run sonar if the secret is present (so not for forks). Updated build requirements for develop and stable docker pushes.

* Fixed env variable

* Okay not possible to do secrets in if statement

* Fixed the build step to output the openapi.json where it's expected.
2022-11-20 12:32:21 -08:00

210 lines
7.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Data;
using API.DTOs.CollectionTags;
using API.Entities.Metadata;
using API.Extensions;
using API.SignalR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
/// <summary>
/// APIs for Collections
/// </summary>
public class CollectionController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IEventHub _eventHub;
/// <inheritdoc />
public CollectionController(IUnitOfWork unitOfWork, IEventHub eventHub)
{
_unitOfWork = unitOfWork;
_eventHub = eventHub;
}
/// <summary>
/// Return a list of all collection tags on the server
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<IEnumerable<CollectionTagDto>> GetAllTags()
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
if (isAdmin)
{
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
}
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id);
}
/// <summary>
/// Searches against the collection tags on the DB and returns matches that meet the search criteria.
/// <remarks>Search strings will be cleaned of certain fields, like %</remarks>
/// </summary>
/// <param name="queryString">Search term</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("search")]
public async Task<IEnumerable<CollectionTagDto>> SearchTags(string queryString)
{
queryString ??= "";
queryString = queryString.Replace(@"%", string.Empty);
if (queryString.Length == 0) return await GetAllTags();
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, user.Id);
}
/// <summary>
/// Checks if a collection exists with the name
/// </summary>
/// <param name="name">If empty or null, will return true as that is invalid</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpGet("name-exists")]
public async Task<ActionResult<bool>> DoesNameExists(string name)
{
if (string.IsNullOrEmpty(name.Trim())) return Ok(true);
return Ok(await _unitOfWork.CollectionTagRepository.TagExists(name));
}
/// <summary>
/// Updates an existing tag with a new title, promotion status, and summary.
/// <remarks>UI does not contain controls to update title</remarks>
/// </summary>
/// <param name="updatedTag"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update")]
public async Task<ActionResult> UpdateTag(CollectionTagDto updatedTag)
{
var existingTag = await _unitOfWork.CollectionTagRepository.GetTagAsync(updatedTag.Id);
if (existingTag == null) return BadRequest("This tag does not exist");
var title = updatedTag.Title.Trim();
if (string.IsNullOrEmpty(title)) return BadRequest("Title cannot be empty");
if (!title.Equals(existingTag.Title) && await _unitOfWork.CollectionTagRepository.TagExists(updatedTag.Title))
return BadRequest("A tag with this name already exists");
existingTag.Title = title;
existingTag.Promoted = updatedTag.Promoted;
existingTag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(updatedTag.Title);
existingTag.Summary = updatedTag.Summary.Trim();
if (_unitOfWork.HasChanges())
{
if (await _unitOfWork.CommitAsync())
{
return Ok("Tag updated successfully");
}
}
else
{
return Ok("Tag updated successfully");
}
return BadRequest("Something went wrong, please try again");
}
/// <summary>
/// Adds a collection tag onto multiple Series. If tag id is 0, this will create a new tag.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-for-series")]
public async Task<ActionResult> AddToMultipleSeries(CollectionTagBulkAddDto dto)
{
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(dto.CollectionTagId);
if (tag == null)
{
tag = DbFactory.CollectionTag(0, dto.CollectionTagTitle, String.Empty, false);
_unitOfWork.CollectionTagRepository.Add(tag);
}
var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIdsAsync(dto.SeriesIds);
foreach (var metadata in seriesMetadatas)
{
if (!metadata.CollectionTags.Any(t => t.Title.Equals(tag.Title, StringComparison.InvariantCulture)))
{
metadata.CollectionTags.Add(tag);
_unitOfWork.SeriesMetadataRepository.Update(metadata);
}
}
if (!_unitOfWork.HasChanges()) return Ok();
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
return BadRequest("There was an issue updating series with collection tag");
}
/// <summary>
/// For a given tag, update the summary if summary has changed and remove a set of series from the tag.
/// </summary>
/// <param name="updateSeriesForTagDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("update-series")]
public async Task<ActionResult> UpdateSeriesForTag(UpdateSeriesForTagDto updateSeriesForTagDto)
{
try
{
var tag = await _unitOfWork.CollectionTagRepository.GetFullTagAsync(updateSeriesForTagDto.Tag.Id);
if (tag == null) return BadRequest("Not a valid Tag");
tag.SeriesMetadatas ??= new List<SeriesMetadata>();
// Check if Tag has updated (Summary)
if (tag.Summary == null || !tag.Summary.Equals(updateSeriesForTagDto.Tag.Summary))
{
tag.Summary = updateSeriesForTagDto.Tag.Summary;
_unitOfWork.CollectionTagRepository.Update(tag);
}
tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked;
if (!updateSeriesForTagDto.Tag.CoverImageLocked)
{
tag.CoverImageLocked = false;
tag.CoverImage = string.Empty;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(tag.Id, MessageFactoryEntityTypes.CollectionTag), false);
_unitOfWork.CollectionTagRepository.Update(tag);
}
foreach (var seriesIdToRemove in updateSeriesForTagDto.SeriesIdsToRemove)
{
tag.SeriesMetadatas.Remove(tag.SeriesMetadatas.Single(sm => sm.SeriesId == seriesIdToRemove));
}
if (tag.SeriesMetadatas.Count == 0)
{
_unitOfWork.CollectionTagRepository.Remove(tag);
}
if (!_unitOfWork.HasChanges()) return Ok("No updates");
if (await _unitOfWork.CommitAsync())
{
return Ok("Tag updated");
}
}
catch (Exception)
{
await _unitOfWork.RollbackAsync();
}
return BadRequest("Something went wrong. Please try again.");
}
}