using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using RMuseum.Models.Auth.Memory; using RMuseum.Models.Ganjoor; using RMuseum.Models.Ganjoor.ViewModels; using RMuseum.Services; using RSecurityBackend.Models.Auth.Memory; using RSecurityBackend.Models.Generic; using System; using System.Linq; using System.Net; using System.Threading.Tasks; namespace RMuseum.Controllers { /// /// related people (family tree / person tagging) - reads are anonymous; the only write paths are /// the suggestion queue below (any logged-in user may suggest an edit to an existing person, /// same as suggesting a poem correction; a moderator reviews it the same way a poem correction is /// reviewed). There is no direct-edit endpoint for GanjoorRelatedPerson. /// [Produces("application/json")] [Route("api/people")] public class GanjoorRelatedPersonController : Controller { /// /// get all people /// /// [HttpGet] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson[]))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPeopleAsync() { var res = await _personService.GetPeopleAsync(); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get people who caption a family tree - the entry points for browsing family trees. /// Registered before the "{id}" route below (and constrained to int there) so this literal /// segment isn't swallowed as an id. /// /// [HttpGet("familytrees")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson[]))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetFamilyTreeRootsAsync() { var res = await _personService.GetFamilyTreeRootsAsync(); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get the whole known network of people (every person with at least one kinship edge or /// non-family tie, plus every one of those edges/ties), for the force-directed "ontology" /// explorer at /PeopleGraph. Registered before the "{id}" route below (and constrained to /// int there) so this literal segment isn't swallowed as an id. /// /// [HttpGet("graph")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonGraphViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPersonGraphAsync() { var res = await _personService.GetPersonGraphAsync(); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get person by id /// /// /// [HttpGet("{id:int}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorRelatedPerson))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPersonAsync(int id) { var res = await _personService.GetPersonAsync(id); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get a person along with all their kinship/affiliation edges, for the read-only /// person/family-tree browsing page /// /// /// [HttpGet("{id:int}/relations")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationsViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPersonRelationsAsync(int id) { var res = await _personService.GetPersonRelationsAsync(id); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get the poems tagged with this person /// /// /// [HttpGet("{id:int}/poems")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(PoemGeoDateTag[]))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetPoemsByPersonAsync(int id) { var res = await _personService.GetPoemsByPersonAsync(id); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get the whole connected kinship component reachable from this person (ancestors, /// descendants, spouses, siblings), for the interactive family-tree chart /// /// /// [HttpGet("{id:int}/familytree")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorFamilyTreeViewModel))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetFamilyTreeAsync(int id) { var res = await _personService.GetFamilyTreeAsync(id); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// suggest an edit to an already-approved person's own fields - any logged-in user, same as /// suggesting a poem correction. Goes into the pending queue; does not change the person. /// /// /// /// [HttpPost("{id:int}/editsuggestion")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonEditSuggestion))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task SuggestPersonEditAsync(int id, [FromBody] GanjoorPersonEditSuggestion suggestion) { suggestion.PersonId = id; suggestion.UserId = new Guid(User.Claims.First(c => c.Type == "UserId").Value); var res = await _personService.SuggestPersonEditAsync(suggestion); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get the next unreviewed person-edit suggestion, for the moderator queue - same permission /// as reviewing a poem correction /// /// /// [HttpGet("editsuggestions/next")] [Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonEditSuggestion))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetNextUnreviewedPersonEditSuggestionAsync(int skip = 0) { var res = await _personService.GetNextUnreviewedPersonEditSuggestionAsync(skip); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); var resCount = await _personService.GetUnreviewedPersonEditSuggestionCountAsync(); if (!string.IsNullOrEmpty(resCount.ExceptionString)) return BadRequest(resCount.ExceptionString); // Paging Header - same shape ReviewEdits.cshtml.cs already knows how to read for poem corrections HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject( new PaginationMetadata() { totalCount = resCount.Result, pageSize = -1, currentPage = -1, hasNextPage = false, hasPreviousPage = false, totalPages = -1 }) ); return Ok(res.Result);//might be null } /// /// apply a moderator's decision to a pending person-edit suggestion - same permission as /// moderating a poem correction /// /// /// /// [HttpPost("editsuggestions/{id:int}/moderate")] [Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonEditSuggestion))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task ModeratePersonEditSuggestionAsync(int id, [FromBody] PersonEditSuggestionModerationViewModel moderation) { Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value); var res = await _personService.ModeratePersonEditSuggestionAsync(userId, id, moderation.Result, moderation.ReviewNote); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get a single kinship edge by its own id, with both sides' names resolved - used by /// /SuggestPersonRelationEdit/{relationId} to show what it's about /// /// /// [HttpGet("relations/{relationId:int}")] [AllowAnonymous] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelation))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetRelationByIdAsync(int relationId) { var res = await _personService.GetRelationByIdAsync(relationId); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// suggest an addition, change or removal of a kinship edge - any logged-in user, same as /// suggesting a person edit. Goes into the pending queue; does not change anything. /// /// /// [HttpPost("relationeditsuggestion")] [Authorize] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationEditSuggestion))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task SuggestPersonRelationEditAsync([FromBody] GanjoorPersonRelationEditSuggestion suggestion) { suggestion.UserId = new Guid(User.Claims.First(c => c.Type == "UserId").Value); var res = await _personService.SuggestPersonRelationEditAsync(suggestion); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// get the next unreviewed relation-edit suggestion, for the moderator queue - same /// permission as reviewing a person edit /// /// /// [HttpGet("relationeditsuggestions/next")] [Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationEditSuggestion))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task GetNextUnreviewedPersonRelationEditSuggestionAsync(int skip = 0) { var res = await _personService.GetNextUnreviewedPersonRelationEditSuggestionAsync(skip); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); var resCount = await _personService.GetUnreviewedPersonRelationEditSuggestionCountAsync(); if (!string.IsNullOrEmpty(resCount.ExceptionString)) return BadRequest(resCount.ExceptionString); // Paging Header - same shape ReviewEdits.cshtml.cs already knows how to read for poem corrections HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject( new PaginationMetadata() { totalCount = resCount.Result, pageSize = -1, currentPage = -1, hasNextPage = false, hasPreviousPage = false, totalPages = -1 }) ); return Ok(res.Result);//might be null } /// /// apply a moderator's decision to a pending relation-edit suggestion - same permission as /// moderating a person edit /// /// /// /// [HttpPost("relationeditsuggestions/{id:int}/moderate")] [Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)] [ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPersonRelationEditSuggestion))] [ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))] public async Task ModeratePersonRelationEditSuggestionAsync(int id, [FromBody] PersonEditSuggestionModerationViewModel moderation) { Guid userId = new Guid(User.Claims.First(c => c.Type == "UserId").Value); var res = await _personService.ModeratePersonRelationEditSuggestionAsync(userId, id, moderation.Result, moderation.ReviewNote); if (!string.IsNullOrEmpty(res.ExceptionString)) return BadRequest(res.ExceptionString); return Ok(res.Result); } /// /// person service /// private readonly IGanjoorRelatedPersonService _personService; /// /// constructor /// /// public GanjoorRelatedPersonController(IGanjoorRelatedPersonService personService) { _personService = personService; } } }