From 92a6c81daf7ac82f4ad9bfc95fcea617607452b0 Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Thu, 3 Dec 2020 14:52:12 +0100 Subject: [PATCH 1/9] Add Collaborators by performing for each loop instead of joining tables Joining the tables by using Include() caused the query to take sometimes up to 10 times as long. This was an issue as it caused drastic slow down when retrieving all projects, retrieving project detail or when searching for projects. --- Repositories/ProjectRepository.cs | 324 +++++++++++++++++------------- 1 file changed, 185 insertions(+), 139 deletions(-) diff --git a/Repositories/ProjectRepository.cs b/Repositories/ProjectRepository.cs index e8498ae9..96acb34b 100644 --- a/Repositories/ProjectRepository.cs +++ b/Repositories/ProjectRepository.cs @@ -16,7 +16,6 @@ */ using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; using Models; using Models.Defaults; using Repositories.Base; @@ -29,6 +28,7 @@ namespace Repositories { + public interface IProjectRepository : IRepository { @@ -38,7 +38,7 @@ Task> GetAllWithUsersAndCollaboratorsAsync( Expression> orderBy = null, bool orderByAsc = true, bool? highlighted = null - ); + ); Task CountAsync(bool? highlighted = null); @@ -54,6 +54,7 @@ Task> SearchAsync( Task SearchCountAsync(string query, bool? highlighted = null); Task FindWithUserAndCollaboratorsAsync(int id); + } public class ProjectRepository : Repository, IProjectRepository @@ -62,106 +63,28 @@ public class ProjectRepository : Repository, IProjectRepository public ProjectRepository(DbContext dbContext) : base(dbContext) { } /// - /// Redact user email from the Project if isPublic setting is set to false - /// - /// The project. - /// - /// Project with possibly redacted email depending on setting - /// - private Project RedactUser(Project project) - { - if(project == null) return null; - - if(project?.User?.IsPublic == false) - { - project.User.Email = Defaults.Privacy.RedactedEmail; - } - return project; - } - - /// - /// Redact user email from the Projects in the list. - /// Email will only be redacted if isPublic setting is set to false. - /// - /// The projects. - /// - /// List of Projects with possibly redacted email depending on setting - /// - private List RedactUser(List projects) - { - for(int i = 0; i < projects.Count; i++) - { - projects[i] = RedactUser(projects[i]); - } - return projects; - } - - /// - /// Find the project async by project id + /// Find the project async by project id /// /// The identifier. /// - /// Project with possibly redacted email + /// Project with possibly redacted email /// public override async Task FindAsync(int id) { Project project = await GetDbSet() - .Where(s => s.Id == id) - .Include(p => p.Collaborators) - .Include(p => p.CallToAction) - .SingleOrDefaultAsync(); + .Where(s => s.Id == id) + .Include(p => p.ProjectIcon) + .Include(p => p.CallToAction) + .SingleOrDefaultAsync(); - return RedactUser(project); - } - - /// - /// Apply query parameters and find project based on these filters - /// - /// The linq queryable object. - /// The amount of objects to skip. - /// The amount of objects to take. - /// The order by expression. - /// if set to true [order by asc]. - /// Boolean if the project should show highlighted. - /// - /// IQueryable Projects based on the given filters - /// - private IQueryable ApplyFilters( - IQueryable queryable, - int? skip, - int? take, - Expression> orderBy, - bool orderByAsc, - bool? highlighted - ) - { - if(highlighted.HasValue) + if(project != null) { - IEnumerable highlightedQueryable = DbContext.Set() - .Where(h => h.StartDate <= DateTime.Now || - h.StartDate == null) - .Where(h => h.EndDate >= DateTime.Now || - h.EndDate == null) - .Select(h => h.ProjectId) - .ToList(); - - queryable = queryable.Where(p => highlightedQueryable.Contains(p.Id) == highlighted.Value); + project.Collaborators = await GetDbSet() + .Where(p => p.ProjectId == project.Id) + .ToListAsync(); } - if(orderBy != null) - { - if(orderByAsc) - { - queryable = queryable.OrderBy(orderBy); - } else - { - queryable = queryable.OrderByDescending(orderBy); - } - } - - if(skip.HasValue) queryable = queryable.Skip(skip.Value); - if(take.HasValue) queryable = queryable.Take(take.Value); - return queryable; + return RedactUser(project); } /// @@ -179,28 +102,35 @@ public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( Expression> orderBy = null, bool orderByAsc = true, bool? highlighted = null - ) + ) { - IQueryable queryable = DbSet - .Include(p => p.User) - .Include(p => p.ProjectIcon) - .Include(p => p.CallToAction) - .Include(p => p.Collaborators); + IQueryable queryableProjects = GetDbSet() + .Include(p => p.ProjectIcon) + .Include(p => p.CallToAction); + queryableProjects = ApplyFilters(queryableProjects, skip, take, orderBy, orderByAsc, highlighted); - queryable = ApplyFilters(queryable, skip, take, orderBy, orderByAsc, highlighted); - List projects = await queryable.ToListAsync(); - return RedactUser(projects); + foreach(Project project in queryableProjects) + { + project.Collaborators = await GetDbSet() + .Where(p => p.ProjectId == project.Id) + .ToListAsync(); + project.User = RedactUser(await GetDbSet() + .Where(p => p.Id == project.UserId) + .FirstOrDefaultAsync()); + } + return await queryableProjects.ToListAsync(); } /// - /// Count the amount of projects matching the filters + /// Count the amount of projects matching the filters /// /// The highlighted filter /// The amount of projects matching the filters public virtual async Task CountAsync(bool? highlighted = null) { - return await ApplyFilters(DbSet, null, null, null, true, highlighted).CountAsync(); + return await ApplyFilters(DbSet, null, null, null, true, highlighted) + .CountAsync(); } /// @@ -222,44 +152,53 @@ public virtual async Task> SearchAsync( bool? highlighted = null ) { - List result = await ApplyFilters(GetProjectQueryable(query), skip, take, orderBy, orderByAsc, highlighted).ToListAsync(); - return result.Where(p => ProjectContainsQuery(p, query)).ToList(); + List result = + await ApplyFilters(GetProjectQueryable(query), skip, take, orderBy, orderByAsc, highlighted) + .ToListAsync(); + return result.Where(p => ProjectContainsQuery(p, query)) + .ToList(); } /// - /// Count the amount of projects matching the filters and the search query + /// Count the amount of projects matching the filters and the search query /// /// The search query /// The highlighted filter /// The amount of projects matching the filters public virtual async Task SearchCountAsync(string query, bool? highlighted = null) { - return await ApplyFilters(GetProjectQueryable(query), null, null, null, true, highlighted).CountAsync(); + return await ApplyFilters(GetProjectQueryable(query), null, null, null, true, highlighted) + .CountAsync(); } /// - /// Retrieve project with user and collaborators async. - /// Project will be redacted if user has that setting configured. + /// Retrieve project with user and collaborators async. + /// Project will be redacted if user has that setting configured. /// /// The identifier. /// - /// Possibly redacted Project object with user and collaborators + /// Possibly redacted Project object with user and collaborators /// public async Task FindWithUserAndCollaboratorsAsync(int id) { Project project = await GetDbSet() - .Include(p => p.User) - .Include(p => p.Collaborators) - .Include(p => p.ProjectIcon) - .Include(p => p.CallToAction) - .Where(p => p.Id == id) - .FirstOrDefaultAsync(); + .Include(p => p.User) + .Include(p => p.ProjectIcon) + .Include(p => p.CallToAction) + .Where(p => p.Id == id) + .FirstOrDefaultAsync(); + if(project != null) + { + project.Collaborators = await GetDbSet() + .Where(p => p.ProjectId == project.Id) + .ToListAsync(); + } return RedactUser(project); } /// - /// Updates the specified entity excluding the user object. + /// Updates the specified entity excluding the user object. /// /// The entity. public override void Update(Project entity) @@ -287,44 +226,151 @@ public override void Update(Project entity) } /// - /// Checks if any of the searchable fields of the project passed contains the provided query. + /// Redact user email from the Project if isPublic setting is set to false + /// + /// The project. + /// + /// Project with possibly redacted email depending on setting + /// + private Project RedactUser(Project project) + { + if(project == null) return null; + + if(project?.User?.IsPublic == false) + { + project.User.Email = Defaults.Privacy.RedactedEmail; + } + return project; + } + + /// + /// Redact user email from the User if isPublic setting is set to false + /// + /// The user. + /// + /// User with possibly redacted email depending on setting + /// + private User RedactUser(User user) + { + if(user.IsPublic == false) + { + user.Email = Defaults.Privacy.RedactedEmail; + } + return user; + } + + /// + /// Redact user email from the Projects in the list. + /// Email will only be redacted if isPublic setting is set to false. + /// + /// The projects. + /// + /// List of Projects with possibly redacted email depending on setting + /// + private List RedactUser(List projects) + { + for(int i = 0; i < projects.Count; i++) + { + projects[i] = RedactUser(projects[i]); + } + return projects; + } + + /// + /// Apply query parameters and find project based on these filters + /// + /// The linq queryable object. + /// The amount of objects to skip. + /// The amount of objects to take. + /// The order by expression. + /// if set to true [order by asc]. + /// Boolean if the project should show highlighted. + /// + /// IQueryable Projects based on the given filters + /// + private IQueryable ApplyFilters( + IQueryable queryable, + int? skip, + int? take, + Expression> orderBy, + bool orderByAsc, + bool? highlighted + ) + { + if(highlighted.HasValue) + { + IEnumerable highlightedQueryable = DbContext.Set() + .Where(h => h.StartDate <= DateTime.Now || + h.StartDate == null) + .Where(h => h.EndDate >= DateTime.Now || + h.EndDate == null) + .Select(h => h.ProjectId) + .ToList(); + + queryable = queryable.Where(p => highlightedQueryable.Contains(p.Id) == highlighted.Value); + } + + if(orderBy != null) + { + if(orderByAsc) + { + queryable = queryable.OrderBy(orderBy); + } else + { + queryable = queryable.OrderByDescending(orderBy); + } + } + + if(skip.HasValue) queryable = queryable.Skip(skip.Value); + if(take.HasValue) queryable = queryable.Take(take.Value); + return queryable; + } + + /// + /// Checks if any of the searchable fields of the project passed contains the provided query. /// /// A Project to search in /// The query to search in the project's searchable fields. - /// A boolean representing whether or not the passed query was found in the searchable fields of the provided project. + /// + /// A boolean representing whether or not the passed query was found in the searchable fields of the provided + /// project. + /// private static bool ProjectContainsQuery(Project project, string query) { Regex regex = new Regex(@$"\b{query}\b", RegexOptions.Singleline | RegexOptions.IgnoreCase); - return new List() - { - project.Name, - project.Description, - project.ShortDescription, - project.Uri, - project.User.Name, - project.Id.ToString() - } - .Any(text => regex.IsMatch(text)); + return new List + { + project.Name, + project.Description, + project.ShortDescription, + project.Uri, + project.User.Name, + project.Id.ToString() + } + .Any(text => regex.IsMatch(text)); } /// - /// Get the project queryable which contains the provided query. + /// Get the project queryable which contains the provided query. /// /// A string to search in the project's fields. /// The filtered IQueryable including the project user. private IQueryable GetProjectQueryable(string query) { return DbSet - .Include(p => p.User) - .Include(i => i.ProjectIcon) - .Include(p => p.CallToAction) - .Where(p => - p.Name.Contains(query) || - p.Description.Contains(query) || - p.ShortDescription.Contains(query) || - p.Uri.Contains(query) || - p.Id.ToString().Equals(query) || - p.User.Name.Contains(query)); + .Include(p => p.User) + .Include(i => i.ProjectIcon) + .Include(p => p.CallToAction) + .Where(p => + p.Name.Contains(query) || + p.Description.Contains(query) || + p.ShortDescription.Contains(query) || + p.Uri.Contains(query) || + p.Id.ToString() + .Equals(query) || + p.User.Name.Contains(query)); } + } + } From 96dbfad0579e3ea881294a881dfb3c9cf3fb76d5 Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Fri, 4 Dec 2020 21:51:15 +0100 Subject: [PATCH 2/9] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5356213..bb013a9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed issue where unused project icons where left in the database & File System - [#271](https://github.com/DigitalExcellence/dex-backend/issues/271) - Refactored Postman CLI files to make them work from Postman folder. - [#304](https://github.com/DigitalExcellence/dex-backend/issues/304) - Fixed issue where searching for a project did not include the project icon. - [#307](https://github.com/DigitalExcellence/dex-backend/issues/307) +- Fixes issue where retrieving projects performed badly due to large amount of collaborators. - [#331](https://github.com/DigitalExcellence/dex-backend/issues/331) ### Security From 4a87f55425358d90c55f776262d2406e4758a4bb Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Sat, 5 Dec 2020 12:38:46 +0100 Subject: [PATCH 3/9] Redact User should return null if no user is given --- Repositories/ProjectRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Repositories/ProjectRepository.cs b/Repositories/ProjectRepository.cs index 96acb34b..b023e123 100644 --- a/Repositories/ProjectRepository.cs +++ b/Repositories/ProjectRepository.cs @@ -252,6 +252,7 @@ private Project RedactUser(Project project) /// private User RedactUser(User user) { + if(user == null) return null; if(user.IsPublic == false) { user.Email = Defaults.Privacy.RedactedEmail; From 50e1809bfd61e218bbb4d365ef031f05d7807600 Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Sat, 5 Dec 2020 12:59:16 +0100 Subject: [PATCH 4/9] Happy tests, happy life --- Repositories/ProjectRepository.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Repositories/ProjectRepository.cs b/Repositories/ProjectRepository.cs index b023e123..e5372cfe 100644 --- a/Repositories/ProjectRepository.cs +++ b/Repositories/ProjectRepository.cs @@ -105,6 +105,7 @@ public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( ) { IQueryable queryableProjects = GetDbSet() + .Include(u => u.User) .Include(p => p.ProjectIcon) .Include(p => p.CallToAction); queryableProjects = ApplyFilters(queryableProjects, skip, take, orderBy, orderByAsc, highlighted); @@ -115,9 +116,6 @@ public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( project.Collaborators = await GetDbSet() .Where(p => p.ProjectId == project.Id) .ToListAsync(); - project.User = RedactUser(await GetDbSet() - .Where(p => p.Id == project.UserId) - .FirstOrDefaultAsync()); } return await queryableProjects.ToListAsync(); } From c68bb826ede2779b9b1219722b1c8b29e227894e Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Sat, 5 Dec 2020 13:04:53 +0100 Subject: [PATCH 5/9] Happy life, happy wife. Fixed issue where I did not redact user --- Repositories/ProjectRepository.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Repositories/ProjectRepository.cs b/Repositories/ProjectRepository.cs index e5372cfe..53f3a674 100644 --- a/Repositories/ProjectRepository.cs +++ b/Repositories/ProjectRepository.cs @@ -116,6 +116,7 @@ public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( project.Collaborators = await GetDbSet() .Where(p => p.ProjectId == project.Id) .ToListAsync(); + project.User = RedactUser(project.User); } return await queryableProjects.ToListAsync(); } From 17d2eaacd4936c983ff7722cd6b38f8b3dca5d07 Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Sat, 5 Dec 2020 14:59:12 +0100 Subject: [PATCH 6/9] Likes now have a datetime and retrieving projects returns likes Likes were not being returned when retrieving projects. Due to performance reasons, we are just returning an array for now with user id + date instead of Like Count and boolean to check if currently logged in user has liked that specific project. In the future we should probably move the like and follow things into their own controller with respective services. The frontend can then make a call to retrieve project meta data, which can take a little longer and should be non-blocking for the frontend. For now, the frontend can count the objects in the likes array and check if a user id is equal to the one that is logged in. Fixes #329. --- API/Configuration/MappingProfile.cs | 2 + API/Controllers/ProjectController.cs | 521 +++++++++--------- API/Resources/ProjectLikesResourceResult.cs | 22 + API/Resources/ProjectResourceResult.cs | 5 + API/Resources/ProjectResultResource.cs | 5 + API/Resources/UserProjectLikeResource.cs | 39 -- .../20201205133647_AddDateToLikes.Designer.cs | 499 +++++++++++++++++ .../20201205133647_AddDateToLikes.cs | 24 + .../ApplicationDbContextModelSnapshot.cs | 73 +-- Models/Project.cs | 2 + Models/ProjectLike.cs | 7 +- Repositories/ProjectRepository.cs | 4 + 12 files changed, 864 insertions(+), 339 deletions(-) create mode 100644 API/Resources/ProjectLikesResourceResult.cs delete mode 100644 API/Resources/UserProjectLikeResource.cs create mode 100644 Data/Migrations/20201205133647_AddDateToLikes.Designer.cs create mode 100644 Data/Migrations/20201205133647_AddDateToLikes.cs diff --git a/API/Configuration/MappingProfile.cs b/API/Configuration/MappingProfile.cs index 37326dc7..e5dc4169 100644 --- a/API/Configuration/MappingProfile.cs +++ b/API/Configuration/MappingProfile.cs @@ -18,6 +18,7 @@ using API.Resources; using AutoMapper; using Models; +using System.Collections.Generic; namespace API.Configuration { @@ -78,6 +79,7 @@ public MappingProfile() CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap(); diff --git a/API/Controllers/ProjectController.cs b/API/Controllers/ProjectController.cs index 89a83922..c94e9e63 100644 --- a/API/Controllers/ProjectController.cs +++ b/API/Controllers/ProjectController.cs @@ -28,44 +28,54 @@ using Serilog; using Services.Services; using System.Collections.Generic; -using System.Data.Common; +using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; +using File = Models.File; namespace API.Controllers { + /// - /// This class is responsible for handling HTTP requests that are related - /// to the projects, for example creating, retrieving, updating or deleting. + /// This class is responsible for handling HTTP requests that are related + /// to the projects, for example creating, retrieving, updating or deleting. /// [Route("api/[controller]")] [ApiController] public class ProjectController : ControllerBase { - private readonly IMapper mapper; - private readonly IProjectService projectService; - private readonly IUserService userService; + private readonly IAuthorizationHelper authorizationHelper; + private readonly ICallToActionOptionService callToActionOptionService; private readonly IFileService fileService; private readonly IFileUploader fileUploader; - private readonly IUserProjectService userProjectService; + private readonly IMapper mapper; + private readonly IProjectService projectService; private readonly IUserProjectLikeService userProjectLikeService; - private readonly ICallToActionOptionService callToActionOptionService; + private readonly IUserProjectService userProjectService; + private readonly IUserService userService; /// - /// Initializes a new instance of the class + /// Initializes a new instance of the class /// /// The project service which is used to communicate with the logic layer. /// The user service which is used to communicate with the logic layer. /// The mapper which is used to convert the resources to the models to the resource results. /// The file service which is used to communicate with the logic layer. - /// The service that handles liked project by users which is used to communicate with the logic layer. - /// The authorization helper which is used to communicate with the authorization helper class. + /// + /// The service that handles liked project by users which is used to communicate with + /// the logic layer. + /// + /// + /// The authorization helper which is used to communicate with the authorization helper + /// class. + /// /// The file uploader service is used to upload the files into the file system - /// The user project service is responsible for users that are following / liking projects. - /// The user project service is responsible for users that are following / liking projects. - /// The file uploader service is used to upload the files into the file system. + /// + /// The user project service is responsible for users that are following / liking + /// projects. + /// /// The call to action option service is used to communicate with the logic layer. public ProjectController(IProjectService projectService, IUserService userService, @@ -75,7 +85,7 @@ public ProjectController(IProjectService projectService, IAuthorizationHelper authorizationHelper, IFileUploader fileUploader, IUserProjectService userProjectService, - ICallToActionOptionService callToActionOptionService) + ICallToActionOptionService callToActionOptionService) { this.projectService = projectService; this.userService = userService; @@ -89,21 +99,25 @@ public ProjectController(IProjectService projectService, } /// - /// This method is responsible for retrieving all projects. + /// This method is responsible for retrieving all projects. /// /// The parameters to filter which is used to sort and paginate the projects. /// This method returns the project result resource. /// This endpoint returns all projects. - /// The 400 Bad Request status code is returned when values inside project filter params resource are invalid. + /// + /// The 400 Bad Request status code is returned when values inside project filter params resource are + /// invalid. + /// [HttpGet] [ProducesResponseType(typeof(ProjectResultsResource), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)] - public async Task GetAllProjects([FromQuery] ProjectFilterParamsResource projectFilterParamsResource) + public async Task GetAllProjects( + [FromQuery] ProjectFilterParamsResource projectFilterParamsResource) { ProblemDetails problem = new ProblemDetails - { - Title = "Invalid search request." - }; + { + Title = "Invalid search request." + }; if(projectFilterParamsResource.Page != null && projectFilterParamsResource.Page < 1) { @@ -129,30 +143,37 @@ public async Task GetAllProjects([FromQuery] ProjectFilterParamsR return BadRequest(problem); } - ProjectFilterParams projectFilterParams = mapper.Map(projectFilterParamsResource); - IEnumerable projects = await projectService.GetAllWithUsersAndCollaboratorsAsync(projectFilterParams); + ProjectFilterParams projectFilterParams = + mapper.Map(projectFilterParamsResource); + IEnumerable projects = + await projectService.GetAllWithUsersAndCollaboratorsAsync(projectFilterParams); IEnumerable results = mapper.Map, IEnumerable>(projects); - - ProjectResultsResource resultsResource = new ProjectResultsResource() - { - Results = results.ToArray(), - Count = results.Count(), - TotalCount = await projectService.ProjectsCount(projectFilterParams), - Page = projectFilterParams.Page, - TotalPages = await projectService.GetProjectsTotalPages(projectFilterParams) - }; + ProjectResultsResource resultsResource = new ProjectResultsResource + { + Results = results.ToArray(), + Count = results.Count(), + TotalCount = + await projectService.ProjectsCount(projectFilterParams), + Page = projectFilterParams.Page, + TotalPages = + await projectService.GetProjectsTotalPages( + projectFilterParams) + }; return Ok(resultsResource); } /// - /// This method is responsible for retrieving a single project. + /// This method is responsible for retrieving a single project. /// /// This method returns the project resource result. /// This endpoint returns the project with the specified id. /// The 400 Bad Request status code is returned when the specified id is invalid. - /// The 404 Not Found status code is returned when the project could not be found with the specified id. + /// + /// The 404 Not Found status code is returned when the project could not be found with the specified + /// id. + /// [HttpGet("{projectId}")] [ProducesResponseType(typeof(ProjectResourceResult), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.BadRequest)] @@ -162,23 +183,25 @@ public async Task GetProject(int projectId) if(projectId < 0) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed getting project.", - Detail = "The Id is smaller then 0 and therefore it could never be a valid project id.", - Instance = "D590A4FE-FDBA-4AE5-B184-BC7395C45D4E" - }; + { + Title = "Failed getting project.", + Detail = + "The Id is smaller then 0 and therefore it could never be a valid project id.", + Instance = "D590A4FE-FDBA-4AE5-B184-BC7395C45D4E" + }; return BadRequest(problem); } - Project project = await projectService.FindWithUserAndCollaboratorsAsync(projectId).ConfigureAwait(false); + Project project = await projectService.FindWithUserAndCollaboratorsAsync(projectId) + .ConfigureAwait(false); if(project == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed getting project.", - Detail = "The project could not be found in the database.", - Instance = "38516C41-4BFB-47BE-A759-1206BE6D2D13" - }; + { + Title = "Failed getting project.", + Detail = "The project could not be found in the database.", + Instance = "38516C41-4BFB-47BE-A759-1206BE6D2D13" + }; return NotFound(problem); } @@ -186,12 +209,14 @@ public async Task GetProject(int projectId) } /// - /// This method is responsible for creating a Project. + /// This method is responsible for creating a Project. /// /// This method returns the project resource result. /// This endpoint returns the created project. - /// The 400 Bad Request status code is returned when the project - /// resource is not specified or failed to save project to the database. + /// + /// The 400 Bad Request status code is returned when the project + /// resource is not specified or failed to save project to the database. + /// [HttpPost] [Authorize] [ProducesResponseType(typeof(ProjectResourceResult), (int) HttpStatusCode.Created)] @@ -201,25 +226,28 @@ public async Task CreateProjectAsync([FromBody] ProjectResource p if(projectResource == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed to create a new project.", - Detail = "The specified project resource was null.", - Instance = "8D3D9119-0D12-4631-B2DC-56494639A849" - }; + { + Title = "Failed to create a new project.", + Detail = "The specified project resource was null.", + Instance = "8D3D9119-0D12-4631-B2DC-56494639A849" + }; return BadRequest(problem); } if(projectResource.CallToAction != null) { - IEnumerable callToActionOptions = await callToActionOptionService.GetCallToActionOptionFromValueAsync(projectResource.CallToAction.OptionValue); + IEnumerable callToActionOptions = + await callToActionOptionService.GetCallToActionOptionFromValueAsync( + projectResource.CallToAction.OptionValue); if(!callToActionOptions.Any()) { ProblemDetails problem = new ProblemDetails - { - Title = "Call to action value was not found.", - Detail = "The specified call to action value was not found while creating the project.", - Instance = "40EE82EB-930F-40C8-AE94-0041F7573FE9" - }; + { + Title = "Call to action value was not found.", + Detail = + "The specified call to action value was not found while creating the project.", + Instance = "40EE82EB-930F-40C8-AE94-0041F7573FE9" + }; return BadRequest(problem); } } @@ -227,19 +255,21 @@ public async Task CreateProjectAsync([FromBody] ProjectResource p Project project = mapper.Map(projectResource); File file = await fileService.FindAsync(projectResource.FileId); - if(projectResource.FileId != 0 && file == null) + if(projectResource.FileId != 0 && + file == null) { ProblemDetails problem = new ProblemDetails - { - Title = "File was not found.", - Detail = "The specified file was not found while creating project.", - Instance = "8CABE64D-6B73-4C88-BBD8-B32FA9FE6EC7" - }; + { + Title = "File was not found.", + Detail = "The specified file was not found while creating project.", + Instance = "8CABE64D-6B73-4C88-BBD8-B32FA9FE6EC7" + }; return BadRequest(problem); } project.ProjectIcon = file; - project.User = await HttpContext.GetContextUser(userService).ConfigureAwait(false); + project.User = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); try { @@ -251,18 +281,18 @@ public async Task CreateProjectAsync([FromBody] ProjectResource p Log.Logger.Error(e, "Database exception"); - ProblemDetails problem = new ProblemDetails() - { - Title = "Failed to save new project.", - Detail = "There was a problem while saving the project to the database.", - Instance = "9FEEF001-F91F-44E9-8090-6106703AB033" - }; + ProblemDetails problem = new ProblemDetails + { + Title = "Failed to save new project.", + Detail = "There was a problem while saving the project to the database.", + Instance = "9FEEF001-F91F-44E9-8090-6106703AB033" + }; return BadRequest(problem); } } /// - /// This method is responsible for updating the project with the specified identifier. + /// This method is responsible for updating the project with the specified identifier. /// /// The project identifier which is used for searching the project. /// The project resource which is used for updating the project. @@ -277,43 +307,48 @@ public async Task CreateProjectAsync([FromBody] ProjectResource p [ProducesResponseType(typeof(ProblemDetails), (int) HttpStatusCode.NotFound)] public async Task UpdateProject(int projectId, [FromBody] ProjectResource projectResource) { - Project project = await projectService.FindAsync(projectId).ConfigureAwait(false); + Project project = await projectService.FindAsync(projectId) + .ConfigureAwait(false); if(project == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed to update project.", - Detail = "The specified project could not be found in the database.", - Instance = "b27d3600-33b0-42a0-99aa-4b2f28ea07bb" - }; + { + Title = "Failed to update project.", + Detail = "The specified project could not be found in the database.", + Instance = "b27d3600-33b0-42a0-99aa-4b2f28ea07bb" + }; return NotFound(problem); } - User user = await HttpContext.GetContextUser(userService).ConfigureAwait(false); + User user = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); bool isAllowed = userService.UserHasScope(user.IdentityId, nameof(Defaults.Scopes.ProjectWrite)); if(!(project.UserId == user.Id || isAllowed)) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed to edit the project.", - Detail = "The user is not allowed to edit the project.", - Instance = "906cd8ad-b75c-4efb-9838-849f99e8026b" - }; + { + Title = "Failed to edit the project.", + Detail = "The user is not allowed to edit the project.", + Instance = "906cd8ad-b75c-4efb-9838-849f99e8026b" + }; return Unauthorized(problem); } if(projectResource.CallToAction != null) { - IEnumerable callToActionOptions = await callToActionOptionService.GetCallToActionOptionFromValueAsync(projectResource.CallToAction.OptionValue); + IEnumerable callToActionOptions = + await callToActionOptionService.GetCallToActionOptionFromValueAsync( + projectResource.CallToAction.OptionValue); if(!callToActionOptions.Any()) { ProblemDetails problem = new ProblemDetails - { - Title = "Call to action value was not found.", - Detail = "The specified call to action value was not found while creating the project.", - Instance = "40EE82EB-930F-40C8-AE94-0041F7573FE9" - }; + { + Title = "Call to action value was not found.", + Detail = + "The specified call to action value was not found while creating the project.", + Instance = "40EE82EB-930F-40C8-AE94-0041F7573FE9" + }; return BadRequest(problem); } } @@ -325,12 +360,14 @@ public async Task UpdateProject(int projectId, [FromBody] Project if(project.ProjectIconId != 0) { File fileToDelete = await fileService.FindAsync(project.ProjectIconId.Value); + // Remove the file from the filesystem fileUploader.DeleteFileFromDirectory(fileToDelete); + // Remove file from DB await fileService.RemoveAsync(project.ProjectIconId.Value); - - + + fileService.Save(); } @@ -340,17 +377,16 @@ public async Task UpdateProject(int projectId, [FromBody] Project if(file != null) { project.ProjectIcon = file; - } else { ProblemDetails problem = new ProblemDetails - { - Title = "File was not found.", - Detail = "The specified file was not found while updating project.", - Instance = "69166D3D-6D34-4050-BD25-71F1BEBE43D3" - }; + { + Title = "File was not found.", + Detail = "The specified file was not found while updating project.", + Instance = "69166D3D-6D34-4050-BD25-71F1BEBE43D3" + }; return BadRequest(problem); - } + } } mapper.Map(projectResource, project); projectService.Update(project); @@ -359,11 +395,14 @@ public async Task UpdateProject(int projectId, [FromBody] Project } /// - /// This method is responsible for deleting a project. + /// This method is responsible for deleting a project. /// /// This method returns the status code 200. /// This endpoint returns status code 200. The project is deleted. - /// The 401 Unauthorized status code is returned when the the user has not the correct permission to delete. + /// + /// The 401 Unauthorized status code is returned when the the user has not the correct permission to + /// delete. + /// /// The 404 Not Found status code is returned when the project to delete was not found. [HttpDelete("{projectId}")] [Authorize] @@ -377,15 +416,16 @@ public async Task DeleteProject(int projectId) if(project == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed to delete the project.", - Detail = "The project could not be found in the database.", - Instance = "AF63CF48-ECAA-4996-BAA0-BF52926D12AC" - }; + { + Title = "Failed to delete the project.", + Detail = "The project could not be found in the database.", + Instance = "AF63CF48-ECAA-4996-BAA0-BF52926D12AC" + }; return NotFound(problem); } - User user = await HttpContext.GetContextUser(userService).ConfigureAwait(false); + User user = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); bool isAllowed = await authorizationHelper.UserIsAllowed(user, nameof(Defaults.Scopes.ProjectWrite), nameof(Defaults.Scopes.InstitutionProjectWrite), @@ -394,11 +434,11 @@ public async Task DeleteProject(int projectId) if(!(project.UserId == user.Id || isAllowed)) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed to delete the project.", - Detail = "The user is not allowed to delete the project.", - Instance = "D0363680-5B4F-40A1-B381-0A7544C70164" - }; + { + Title = "Failed to delete the project.", + Detail = "The user is not allowed to delete the project.", + Instance = "D0363680-5B4F-40A1-B381-0A7544C70164" + }; return Unauthorized(problem); } @@ -410,20 +450,19 @@ public async Task DeleteProject(int projectId) { // Remove the file from the database await fileService.RemoveAsync(fileToDelete.Id) - .ConfigureAwait(false); + .ConfigureAwait(false); fileService.Save(); // Remove the file from the filesystem fileUploader.DeleteFileFromDirectory(fileToDelete); - - } catch(System.IO.FileNotFoundException) + } catch(FileNotFoundException) { ProblemDetails problem = new ProblemDetails - { - Title = "File could not be deleted because the path does not exist.", - Detail = "File could not be found.", - Instance = "367594c4-1fab-47ae-beb4-a41b53c65a18" - }; + { + Title = "File could not be deleted because the path does not exist.", + Detail = "File could not be found.", + Instance = "367594c4-1fab-47ae-beb4-a41b53c65a18" + }; return NotFound(problem); } @@ -437,7 +476,7 @@ await projectService.RemoveAsync(projectId) } /// - /// Follows a project with given projectId and gets userId + /// Follows a project with given projectId and gets userId /// /// /// 200 if success 409 if user already follows project @@ -445,27 +484,28 @@ await projectService.RemoveAsync(projectId) [Authorize] public async Task FollowProject(int projectId) { - User user = await HttpContext.GetContextUser(userService).ConfigureAwait(false); + User user = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); if(await userService.FindAsync(user.Id) == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed getting the user account.", - Detail = "The database does not contain a user with this user id.", - Instance = "B778C55A-D41E-4101-A7A0-F02F76E5A6AE" - }; + { + Title = "Failed getting the user account.", + Detail = "The database does not contain a user with this user id.", + Instance = "B778C55A-D41E-4101-A7A0-F02F76E5A6AE" + }; return NotFound(problem); } if(userProjectService.CheckIfUserFollows(user.Id, projectId)) { ProblemDetails problem = new ProblemDetails - { - Title = "User already follows this project", - Detail = "You are already following this project.", - Instance = "27D14082-9906-4EB8-AE4C-65BAEC0BB4FD" - }; + { + Title = "User already follows this project", + Detail = "You are already following this project.", + Instance = "27D14082-9906-4EB8-AE4C-65BAEC0BB4FD" + }; return Conflict(problem); } @@ -474,22 +514,22 @@ public async Task FollowProject(int projectId) if(await projectService.FindAsync(projectId) == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed getting the project.", - Detail = "The database does not contain a project with this project id.", - Instance = "57C13F73-6D22-41F3-AB05-0CCC1B3C8328" - }; + { + Title = "Failed getting the project.", + Detail = "The database does not contain a project with this project id.", + Instance = "57C13F73-6D22-41F3-AB05-0CCC1B3C8328" + }; return NotFound(problem); } UserProject userProject = new UserProject(project, user); userProjectService.Add(userProject); userProjectService.Save(); - return Ok(mapper.Map(userProject)); + return Ok(mapper.Map(userProject)); } /// - /// Unfollows project + /// Unfollows project /// /// /// @@ -497,27 +537,28 @@ public async Task FollowProject(int projectId) [Authorize] public async Task UnfollowProject(int projectId) { - User user = await HttpContext.GetContextUser(userService).ConfigureAwait(false); + User user = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); if(await userService.FindAsync(user.Id) == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed getting the user account.", - Detail = "The database does not contain a user with this user id.", - Instance = "B778C55A-D41E-4101-A7A0-F02F76E5A6AE" - }; + { + Title = "Failed getting the user account.", + Detail = "The database does not contain a user with this user id.", + Instance = "B778C55A-D41E-4101-A7A0-F02F76E5A6AE" + }; return NotFound(problem); } if(userProjectService.CheckIfUserFollows(user.Id, projectId) == false) { ProblemDetails problem = new ProblemDetails - { - Title = "User is not following this project", - Detail = "You are not following this project.", - Instance = "27D14082-9906-4EB8-AE4C-65BAEC0BB4FD" - }; + { + Title = "User is not following this project", + Detail = "You are not following this project.", + Instance = "27D14082-9906-4EB8-AE4C-65BAEC0BB4FD" + }; return Conflict(problem); } @@ -526,11 +567,11 @@ public async Task UnfollowProject(int projectId) if(await projectService.FindAsync(projectId) == null) { ProblemDetails problem = new ProblemDetails - { - Title = "Failed getting the project.", - Detail = "The database does not contain a project with this project id.", - Instance = "57C13F73-6D22-41F3-AB05-0CCC1B3C8328" - }; + { + Title = "Failed getting the project.", + Detail = "The database does not contain a project with this project id.", + Instance = "57C13F73-6D22-41F3-AB05-0CCC1B3C8328" + }; return NotFound(problem); } UserProject userProject = new UserProject(project, user); @@ -553,82 +594,64 @@ public async Task UnfollowProject(int projectId) [Authorize] public async Task LikeProject(int projectId) { - User currentUser = await - HttpContext.GetContextUser(userService) - .ConfigureAwait(false); + User currentUser = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); - if(await userService.FindAsync(currentUser.Id) == null) + if(currentUser == null) { ProblemDetails problemDetails = new ProblemDetails - { - Title = "Failed to getting the user account.", - Detail = "The database does not contain a user with the provided user id.", - Instance = "F8DB2F94-48DA-4FEB-9BDA-FF24A59333C1" - }; + { + Title = "Failed to getting the user account.", + Detail = + "The database does not contain a user with the provided user id.", + Instance = "F8DB2F94-48DA-4FEB-9BDA-FF24A59333C1" + }; return NotFound(problemDetails); } if(userProjectLikeService.CheckIfUserAlreadyLiked(currentUser.Id, projectId)) { ProblemDetails problemDetails = new ProblemDetails - { - Title = "User already liked this project", - Detail = "You are already liked this project.", - Instance = "5B0104E2-C864-4ADB-9321-32CD352DC124" - }; + { + Title = "User already liked this project", + Detail = "You already liked this project.", + Instance = "5B0104E2-C864-4ADB-9321-32CD352DC124" + }; return Conflict(problemDetails); } - Project projectToUnlike = new Project(); + Project projectToLike = await projectService.FindAsync(projectId); - try + if(projectToLike == null) { - projectToUnlike = await projectService.FindAsync(projectId); - - if(await projectService.FindAsync(projectId) == null) - { - ProblemDetails problemDetails = new ProblemDetails - { - Title = "Failed to getting the project.", - Detail = "The database does not contain a project with the provided project id.", - Instance = "711B2DDE-D028-479E-8CB7-33F587478F8F" - }; - return NotFound(problemDetails); - } - } catch(DbException e) - { - Log.Logger.Error(e, "Database exception!"); - - ProblemDetails problemDetails = new ProblemDetails() + ProblemDetails problemDetails = new ProblemDetails { - Title = - "Could not find the project with provided id =>" + projectId, - Detail = "The database failed to retrieve the project.", - Instance = "DA92E268-9DFA-4CEE-91BC-E042F3A1AA9C" + Title = "Failed to getting the project.", + Detail = + "The database does not contain a project with the provided project id.", + Instance = "711B2DDE-D028-479E-8CB7-33F587478F8F" }; - - return BadRequest(problemDetails); + return NotFound(problemDetails); } try { - ProjectLike projectLike = - new ProjectLike(projectToUnlike, currentUser); - await userProjectLikeService.AddAsync(projectLike); + ProjectLike like = new ProjectLike(projectToLike, currentUser); + await userProjectLikeService.AddAsync(like) + .ConfigureAwait(false); userProjectLikeService.Save(); - return Ok(mapper.Map(projectLike)); + return Ok(mapper.Map(like)); } catch(DbUpdateException e) { - Log.Logger.Error(e,"Database exception!"); + Log.Logger.Error(e, "Database exception!"); ProblemDetails problemDetails = new ProblemDetails - { - Title = "Could not create the liked project details.", - Detail = "The database failed to save the liked project.", - Instance = "F941879E-6C25-4A35-A962-8E86382E1849" - }; + { + Title = "Could not create the liked project details.", + Detail = "The database failed to save the liked project.", + Instance = "F941879E-6C25-4A35-A962-8E86382E1849" + }; return BadRequest(problemDetails); } } @@ -646,84 +669,54 @@ public async Task LikeProject(int projectId) [Authorize] public async Task UnlikeProject(int projectId) { - User currentUser = await - HttpContext.GetContextUser(userService) - .ConfigureAwait(false); + User currentUser = await HttpContext.GetContextUser(userService) + .ConfigureAwait(false); - if(await userService.FindAsync(currentUser.Id) == null) + if(currentUser == null) { ProblemDetails problemDetails = new ProblemDetails - { - Title = "Failed to getting the user account.", - Detail = "The database does not contain a user with the provided user id.", - Instance = "F8DB2F94-48DA-4FEB-9BDA-FF24A59333C1" - }; + { + Title = "Failed to getting the user account.", + Detail = + "The database does not contain a user with the provided user id.", + Instance = "F8DB2F94-48DA-4FEB-9BDA-FF24A59333C1" + }; return NotFound(problemDetails); } if(!userProjectLikeService.CheckIfUserAlreadyLiked(currentUser.Id, projectId)) { ProblemDetails problemDetails = new ProblemDetails - { - Title = "User didn't liked this project.", - Detail = "You did not like this project at the moment.", - Instance = "03590F81-C06D-4707-A646-B9B7F79B8A15" - }; + { + Title = "User didn't like this project.", + Detail = "You did not like this project at the moment.", + Instance = "03590F81-C06D-4707-A646-B9B7F79B8A15" + }; return Conflict(problemDetails); } - Project projectToLike = new Project(); - try - { - projectToLike = await projectService.FindAsync(projectId); + Project projectToLike = await projectService.FindAsync(projectId); - if(await projectService.FindAsync(projectId) == null) - { - ProblemDetails problemDetails = new ProblemDetails - { - Title = "Failed to getting the project.", - Detail = "The database does not contain a project with the provided project id.", - Instance = "711B2DDE-D028-479E-8CB7-33F587478F8F" - }; - return NotFound(problemDetails); - } - } catch(DbException e) + if(projectToLike == null) { - Log.Logger.Error(e, "Database exception!"); - - ProblemDetails problemDetails = new ProblemDetails() + ProblemDetails problemDetails = new ProblemDetails { - Title = - "Could not find the project with provided id =>" + projectId, - Detail = "The database failed to retrieve the project.", - Instance = "DA92E268-9DFA-4CEE-91BC-E042F3A1AA9C" + Title = "Failed to getting the project.", + Detail = + "The database does not contain a project with the provided project id.", + Instance = "711B2DDE-D028-479E-8CB7-33F587478F8F" }; - - return BadRequest(problemDetails); + return NotFound(problemDetails); } - try - { - ProjectLike projectLike = - new ProjectLike(projectToLike, currentUser); - userProjectLikeService.Remove(projectLike); - - userProjectLikeService.Save(); - return Ok(mapper.Map(projectLike)); - } catch(DbUpdateException e) - { - Log.Logger.Error(e,"Database exception!"); + ProjectLike projectLike = new ProjectLike(projectToLike, currentUser); + userProjectLikeService.Remove(projectLike); - ProblemDetails problemDetails = new ProblemDetails - { - Title = "Could not remove the liked project details.", - Detail = "The database failed to remove the liked project.", - Instance = "78C59017-5846-4564-9E79-E804E2E29E59" - }; - return BadRequest(problemDetails); - } + userProjectLikeService.Save(); + return Ok(mapper.Map(projectLike)); } + } + } diff --git a/API/Resources/ProjectLikesResourceResult.cs b/API/Resources/ProjectLikesResourceResult.cs new file mode 100644 index 00000000..e72c4013 --- /dev/null +++ b/API/Resources/ProjectLikesResourceResult.cs @@ -0,0 +1,22 @@ +using System; + +namespace API.Resources +{ + /// + /// The view model result of UserProjectLike + /// + public class ProjectLikesResourceResult + { + /// + /// Gets or sets the id of the user who liked the project. + /// + /// + /// The User identifier + /// + public int UserId { get; set; } + /// + /// Gets or sets the date of when the user has liked the project + /// + public DateTime Date { get; set; } + } +} diff --git a/API/Resources/ProjectResourceResult.cs b/API/Resources/ProjectResourceResult.cs index 08b89f2b..84a37d04 100644 --- a/API/Resources/ProjectResourceResult.cs +++ b/API/Resources/ProjectResourceResult.cs @@ -15,6 +15,7 @@ * If not, see https://www.gnu.org/licenses/lgpl-3.0.txt */ +using Models; using System; using System.Collections.Generic; @@ -82,5 +83,9 @@ public class ProjectResourceResult /// This gets or sets the call to action /// public CallToActionResourceResult CallToAction { get; set; } + /// + /// This gets or sets the likes of the project + /// + public List Likes { get; set; } } } diff --git a/API/Resources/ProjectResultResource.cs b/API/Resources/ProjectResultResource.cs index e10e9569..6c78f010 100644 --- a/API/Resources/ProjectResultResource.cs +++ b/API/Resources/ProjectResultResource.cs @@ -15,6 +15,7 @@ * If not, see https://www.gnu.org/licenses/lgpl-3.0.txt */ +using Models; using System; using System.Collections.Generic; @@ -69,6 +70,10 @@ public class ProjectResultResource /// This gets or sets the call to action of the project. /// public CallToActionResourceResult CallToAction { get; set; } + /// + /// This gets or sets the likes of the project. + /// + public List Likes { get; set; } } } diff --git a/API/Resources/UserProjectLikeResource.cs b/API/Resources/UserProjectLikeResource.cs deleted file mode 100644 index 83fefec7..00000000 --- a/API/Resources/UserProjectLikeResource.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Models; - -namespace API.Resources -{ - /// - /// The view model result of UserProjectLike - /// - public class UserProjectLikeResource - { - /// - /// Gets or sets the Id of the resource result. - /// - /// - /// The identifier. - /// - public int Id { get; set; } - /// - /// Gets or sets the project being liked of the resource result. - /// - /// - /// The Project class instance - /// - public Project LikedProject { get; set; } - /// - /// Gets or sets the user who created the project - /// - /// - /// The User class instance - /// - public User CreatorOfProject { get; set; } - /// - /// Gets or sets the id of the user who liked the project. - /// - /// - /// The User identifier - /// - public int UserId { get; set; } - } -} diff --git a/Data/Migrations/20201205133647_AddDateToLikes.Designer.cs b/Data/Migrations/20201205133647_AddDateToLikes.Designer.cs new file mode 100644 index 00000000..2b2a4463 --- /dev/null +++ b/Data/Migrations/20201205133647_AddDateToLikes.Designer.cs @@ -0,0 +1,499 @@ +// +using System; +using Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace _4_Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20201205133647_AddDateToLikes")] + partial class AddDateToLikes + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Models.CallToAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("OptionValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CallToAction"); + }); + + modelBuilder.Entity("Models.CallToActionOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CallToActionOption"); + }); + + modelBuilder.Entity("Models.Collaborator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FullName") + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("Role") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Collaborators"); + }); + + modelBuilder.Entity("Models.EmbeddedProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Guid") + .HasColumnType("uniqueidentifier"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("EmbeddedProject"); + }); + + modelBuilder.Entity("Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UploadDateTime") + .HasColumnType("datetime2"); + + b.Property("UploaderId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UploaderId"); + + b.ToTable("File"); + }); + + modelBuilder.Entity("Models.Highlight", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Highlight"); + }); + + modelBuilder.Entity("Models.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("IdentityId") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Institution"); + }); + + modelBuilder.Entity("Models.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CallToActionId") + .HasColumnType("int"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProjectIconId") + .HasColumnType("int"); + + b.Property("ShortDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CallToActionId"); + + b.HasIndex("ProjectIconId"); + + b.HasIndex("UserId"); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("LikedProjectId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LikedProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("ProjectLike"); + }); + + modelBuilder.Entity("Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("Models.RoleScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("Scope") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleScope"); + }); + + modelBuilder.Entity("Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IdentityId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InstitutionId") + .HasColumnType("int"); + + b.Property("IsPublic") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ProfileUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstitutionId"); + + b.HasIndex("RoleId"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Models.UserProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserProject"); + }); + + modelBuilder.Entity("Models.UserUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("FollowedUserId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("FollowedUserId"); + + b.HasIndex("UserId"); + + b.ToTable("UserUser"); + }); + + modelBuilder.Entity("Models.Collaborator", b => + { + b.HasOne("Models.Project", null) + .WithMany("Collaborators") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.EmbeddedProject", b => + { + b.HasOne("Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.File", b => + { + b.HasOne("Models.User", "Uploader") + .WithMany() + .HasForeignKey("UploaderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.Highlight", b => + { + b.HasOne("Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.Project", b => + { + b.HasOne("Models.CallToAction", "CallToAction") + .WithMany() + .HasForeignKey("CallToActionId"); + + b.HasOne("Models.File", "ProjectIcon") + .WithMany() + .HasForeignKey("ProjectIconId"); + + b.HasOne("Models.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.ProjectLike", b => + { + b.HasOne("Models.Project", "LikedProject") + .WithMany("Likes") + .HasForeignKey("LikedProjectId"); + + b.HasOne("Models.User", "CreatorOfProject") + .WithMany("LikedProjectsByUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.RoleScope", b => + { + b.HasOne("Models.Role", null) + .WithMany("Scopes") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.User", b => + { + b.HasOne("Models.Institution", "Institution") + .WithMany() + .HasForeignKey("InstitutionId"); + + b.HasOne("Models.Role", "Role") + .WithMany() + .HasForeignKey("RoleId"); + }); + + modelBuilder.Entity("Models.UserProject", b => + { + b.HasOne("Models.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId"); + + b.HasOne("Models.User", "User") + .WithMany("UserProject") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Models.UserUser", b => + { + b.HasOne("Models.User", "FollowedUser") + .WithMany("FollowedUsers") + .HasForeignKey("FollowedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/20201205133647_AddDateToLikes.cs b/Data/Migrations/20201205133647_AddDateToLikes.cs new file mode 100644 index 00000000..8fb4f282 --- /dev/null +++ b/Data/Migrations/20201205133647_AddDateToLikes.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace _4_Data.Migrations +{ + public partial class AddDateToLikes : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Date", + table: "ProjectLike", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Date", + table: "ProjectLike"); + } + } +} diff --git a/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Data/Migrations/ApplicationDbContextModelSnapshot.cs index c2d36d06..b7f696b5 100644 --- a/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -182,28 +182,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Institution"); }); - modelBuilder.Entity("Models.ProjectLike", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("LikedProjectId") - .HasColumnType("int"); - - b.Property("UserId") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("LikedProjectId"); - - b.HasIndex("UserId"); - - b.ToTable("ProjectLike"); - }); - modelBuilder.Entity("Models.Project", b => { b.Property("Id") @@ -252,6 +230,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Project"); }); + modelBuilder.Entity("Models.ProjectLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Date") + .HasColumnType("datetime2"); + + b.Property("LikedProjectId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LikedProjectId"); + + b.HasIndex("UserId"); + + b.ToTable("ProjectLike"); + }); + modelBuilder.Entity("Models.Role", b => { b.Property("Id") @@ -413,19 +416,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("Models.ProjectLike", b => - { - b.HasOne("Models.Project", "LikedProject") - .WithMany() - .HasForeignKey("LikedProjectId"); - - b.HasOne("Models.User", "CreatorOfProject") - .WithMany("LikedProjectsByUsers") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("Models.Project", b => { b.HasOne("Models.CallToAction", "CallToAction") @@ -443,6 +433,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Models.ProjectLike", b => + { + b.HasOne("Models.Project", "LikedProject") + .WithMany("Likes") + .HasForeignKey("LikedProjectId"); + + b.HasOne("Models.User", "CreatorOfProject") + .WithMany("LikedProjectsByUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Models.RoleScope", b => { b.HasOne("Models.Role", null) diff --git a/Models/Project.cs b/Models/Project.cs index 28ec13fa..892fda4a 100644 --- a/Models/Project.cs +++ b/Models/Project.cs @@ -61,6 +61,8 @@ public Project() public CallToAction CallToAction { get; set; } + public List Likes { get; set; } + } } diff --git a/Models/ProjectLike.cs b/Models/ProjectLike.cs index 10061f89..b7fdba2c 100644 --- a/Models/ProjectLike.cs +++ b/Models/ProjectLike.cs @@ -1,3 +1,6 @@ +using Microsoft.VisualBasic; +using System; + namespace Models { /// @@ -11,11 +14,11 @@ public ProjectLike(Project likedProject, User creatorOfProject) { LikedProject = likedProject; CreatorOfProject = creatorOfProject; + Date = DateTime.Now; } public ProjectLike() { - } /// @@ -50,6 +53,8 @@ public ProjectLike() /// The user identifier. /// public int UserId { get; set; } + + public DateTime Date { get; set; } } } diff --git a/Repositories/ProjectRepository.cs b/Repositories/ProjectRepository.cs index 96acb34b..3da48748 100644 --- a/Repositories/ProjectRepository.cs +++ b/Repositories/ProjectRepository.cs @@ -82,6 +82,7 @@ public override async Task FindAsync(int id) project.Collaborators = await GetDbSet() .Where(p => p.ProjectId == project.Id) .ToListAsync(); + project.Likes = await GetDbSet().Where(p => p.LikedProject.Id == project.Id).ToListAsync(); } return RedactUser(project); @@ -118,6 +119,7 @@ public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( project.User = RedactUser(await GetDbSet() .Where(p => p.Id == project.UserId) .FirstOrDefaultAsync()); + project.Likes = await GetDbSet().Where(p => p.LikedProject.Id == project.Id).ToListAsync(); } return await queryableProjects.ToListAsync(); } @@ -192,6 +194,7 @@ public async Task FindWithUserAndCollaboratorsAsync(int id) project.Collaborators = await GetDbSet() .Where(p => p.ProjectId == project.Id) .ToListAsync(); + project.Likes = await GetDbSet().Where(p => p.LikedProject.Id == project.Id).ToListAsync(); } return RedactUser(project); @@ -361,6 +364,7 @@ private IQueryable GetProjectQueryable(string query) .Include(p => p.User) .Include(i => i.ProjectIcon) .Include(p => p.CallToAction) + .Include(l => l.Likes) .Where(p => p.Name.Contains(query) || p.Description.Contains(query) || From b0d4fdc70a8d6423944cc95541b6bf6d0f948549 Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Sat, 5 Dec 2020 15:11:12 +0100 Subject: [PATCH 7/9] Update CHANGELOG.md --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb013a9c..256d7755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Automatically link users to their institution - [#295](https://github.com/DigitalExcellence/dex-backend/issues/295) -- Added call to actions for projects and call to action options - [312](https://github.com/DigitalExcellence/dex-backend/issues/312) -- Collaborators are now included on the project overview page - [#317](https://github.com/DigitalExcellence/dex-backend/issues/317) -- Added new endpoint for ability to like and unlike projects - [#229](https://github.com/DigitalExcellence/dex-backend/issues/229) +- Automatically link users to their institution. - [#295](https://github.com/DigitalExcellence/dex-backend/issues/295) +- Added call to actions for projects and call to action options. - [312](https://github.com/DigitalExcellence/dex-backend/issues/312) +- Collaborators are now included on the project overview page. - [#317](https://github.com/DigitalExcellence/dex-backend/issues/317) +- Added new endpoint for ability to like and unlike projects. - [#229](https://github.com/DigitalExcellence/dex-backend/issues/229) +- Project retrieval endpoints now include likes. - [#329](https://github.com/DigitalExcellence/dex-backend/issues/329) ### Changed @@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fixed issue where unused project icons where left in the database & File System - [#271](https://github.com/DigitalExcellence/dex-backend/issues/271) +- Fixed issue where unused project icons where left in the database & File System. - [#271](https://github.com/DigitalExcellence/dex-backend/issues/271) - Refactored Postman CLI files to make them work from Postman folder. - [#304](https://github.com/DigitalExcellence/dex-backend/issues/304) - Fixed issue where searching for a project did not include the project icon. - [#307](https://github.com/DigitalExcellence/dex-backend/issues/307) - Fixes issue where retrieving projects performed badly due to large amount of collaborators. - [#331](https://github.com/DigitalExcellence/dex-backend/issues/331) From 83e93602fae8a5cb851d3c1d60c403a11b2a9c4d Mon Sep 17 00:00:00 2001 From: Brend Smits Date: Sat, 5 Dec 2020 15:20:07 +0100 Subject: [PATCH 8/9] Added Response Types for improved documentation --- API/Controllers/ProjectController.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/API/Controllers/ProjectController.cs b/API/Controllers/ProjectController.cs index c94e9e63..5f7bcfdc 100644 --- a/API/Controllers/ProjectController.cs +++ b/API/Controllers/ProjectController.cs @@ -21,6 +21,7 @@ using API.Resources; using AutoMapper; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Models; @@ -482,6 +483,9 @@ await projectService.RemoveAsync(projectId) /// 200 if success 409 if user already follows project [HttpPost("follow/{projectId}")] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task FollowProject(int projectId) { User user = await HttpContext.GetContextUser(userService) @@ -535,6 +539,9 @@ public async Task FollowProject(int projectId) /// [HttpDelete("follow/{projectId}")] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task UnfollowProject(int projectId) { User user = await HttpContext.GetContextUser(userService) @@ -592,6 +599,10 @@ public async Task UnfollowProject(int projectId) /// [HttpPost("like/{projectId}")] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] public async Task LikeProject(int projectId) { User currentUser = await HttpContext.GetContextUser(userService) @@ -667,6 +678,9 @@ await userProjectLikeService.AddAsync(like) /// [HttpDelete("like/{projectId}")] [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] public async Task UnlikeProject(int projectId) { User currentUser = await HttpContext.GetContextUser(userService) From cf5d547202737865b8f374a22702427b8e90b32b Mon Sep 17 00:00:00 2001 From: Ruben Date: Mon, 7 Dec 2020 10:18:20 +0100 Subject: [PATCH 9/9] Updated documentation --- API/Resources/ProjectLikesResourceResult.cs | 2 +- API/Resources/ProjectResourceResult.cs | 28 ++--- Repositories/ProjectRepository.cs | 114 ++++++++++---------- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/API/Resources/ProjectLikesResourceResult.cs b/API/Resources/ProjectLikesResourceResult.cs index e72c4013..3c2d0ab4 100644 --- a/API/Resources/ProjectLikesResourceResult.cs +++ b/API/Resources/ProjectLikesResourceResult.cs @@ -15,7 +15,7 @@ public class ProjectLikesResourceResult /// public int UserId { get; set; } /// - /// Gets or sets the date of when the user has liked the project + /// Gets or sets the date of when the user has liked the project. /// public DateTime Date { get; set; } } diff --git a/API/Resources/ProjectResourceResult.cs b/API/Resources/ProjectResourceResult.cs index 84a37d04..d89c2bbc 100644 --- a/API/Resources/ProjectResourceResult.cs +++ b/API/Resources/ProjectResourceResult.cs @@ -22,69 +22,69 @@ namespace API.Resources { /// - /// The view model result of project + /// The view model result of project. /// public class ProjectResourceResult { /// - /// Get or Set Id of a Project Resource Reuslt + /// Get or Set Id of a Project Resource Result. /// public int Id { get; set; } /// - /// This gets or sets the user + /// This gets or sets the user. /// public LimitedUserResourceResult User { get; set; } /// - /// This gets or sets the userId + /// This gets or sets the userId. /// public int UserId { get; set; } /// - /// This gets or sets the Name + /// This gets or sets the Name. /// public string Name { get; set; } /// - /// This gets or sets the Description + /// This gets or sets the Description. /// public string Description { get; set; } /// - /// This gets or sets the Short Description + /// This gets or sets the Short Description. /// public string ShortDescription { get; set; } /// - /// This gets or sets the Uri + /// This gets or sets the Uri. /// public string Uri { get; set; } /// - /// This gets or sets the collaborators + /// This gets or sets the collaborators. /// public ICollection Collaborators { get; set; } /// - /// This gets or sets the Created time of the project + /// This gets or sets the Created time of the project. /// public DateTime Created { get; set; } /// - /// This gets or sets the Updated time of the project + /// This gets or sets the Updated time of the project. /// public DateTime Updated { get; set; } /// - /// This gets or set the file of the project + /// This gets or set the file of the project. /// public FileResourceResult ProjectIcon { get; set; } /// - /// This gets or sets the call to action + /// This gets or sets the call to action. /// public CallToActionResourceResult CallToAction { get; set; } /// - /// This gets or sets the likes of the project + /// This gets or sets the likes of the project. /// public List Likes { get; set; } } diff --git a/Repositories/ProjectRepository.cs b/Repositories/ProjectRepository.cs index cf912b23..08af0395 100644 --- a/Repositories/ProjectRepository.cs +++ b/Repositories/ProjectRepository.cs @@ -63,11 +63,11 @@ public class ProjectRepository : Repository, IProjectRepository public ProjectRepository(DbContext dbContext) : base(dbContext) { } /// - /// Find the project async by project id + /// This method finds the project async by project the specified id. /// - /// The identifier. + /// The unique identifier which is used for searching the correct project. /// - /// Project with possibly redacted email + /// This method returns a project with the specified id with possibly redacted email. /// public override async Task FindAsync(int id) { @@ -89,14 +89,14 @@ public override async Task FindAsync(int id) } /// - /// Get the projects in the database + /// This method gets all the projects in the database. /// - /// The number of projects to skip - /// The number of projects to return - /// The property to order the projects by - /// The order direction (True: asc, False: desc) - /// Filter highlighted projects - /// The projects filtered by the parameters + /// The skip parameter represents the number of projects to skip. + /// The take parameter represents the number of projects to return. + /// The order by parameter represents the way how to order the projects. + /// The order by asc parameters represents the order direction (True: asc, False: desc) + /// The highlighted parameter represents the whether to filter highlighted projects. + /// This method returns a list of projects filtered by the specified parameters. public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( int? skip = null, int? take = null, @@ -124,10 +124,10 @@ public virtual async Task> GetAllWithUsersAndCollaboratorsAsync( } /// - /// Count the amount of projects matching the filters + /// This method counts the amount of projects matching the filters. /// - /// The highlighted filter - /// The amount of projects matching the filters + /// The highlighted parameter represents whether to filter highlighted projects. + /// This method returns the amount of projects matching the filters. public virtual async Task CountAsync(bool? highlighted = null) { return await ApplyFilters(DbSet, null, null, null, true, highlighted) @@ -135,15 +135,15 @@ public virtual async Task CountAsync(bool? highlighted = null) } /// - /// Search the database for projects matching the search query and parameters + /// This method searches the database for projects matching the search query and parameters. /// - /// The search query - /// The number of projects to skip - /// The number of projects to return - /// The property to order the projects by - /// The order direction (True: asc, False: desc) - /// Filter highlighted projects - /// The projects matching the search query and parameters + /// The query parameters represents the search query used for filtering projects. + /// The skip parameter represents the number of projects to skip. + /// The take parameter represents the number of projects to return. + /// The order by parameter represents the way how to order the projects. + /// The order by asc parameters represents the order direction (True: asc, False: desc) + /// The highlighted parameter represents the whether to filter highlighted projects. + /// This method returns thee projects matching the search query and parameters. public virtual async Task> SearchAsync( string query, int? skip = null, @@ -161,11 +161,11 @@ await ApplyFilters(GetProjectQueryable(query), skip, take, orderBy, orderByAsc, } /// - /// Count the amount of projects matching the filters and the search query + /// This method counts the amount of projects matching the filters and the search query. /// - /// The search query - /// The highlighted filter - /// The amount of projects matching the filters + /// The query parameters represents the search query used for filtering projects. + /// The highlighted parameter represents the whether to filter highlighted projects. + /// This method returns the amount of projects matching the filters. public virtual async Task SearchCountAsync(string query, bool? highlighted = null) { return await ApplyFilters(GetProjectQueryable(query), null, null, null, true, highlighted) @@ -173,12 +173,12 @@ public virtual async Task SearchCountAsync(string query, bool? highlighted } /// - /// Retrieve project with user and collaborators async. - /// Project will be redacted if user has that setting configured. + /// This method will retrieve a project with user and collaborators async. Project will be redacted if user + /// has that setting configured. /// - /// The identifier. +/// The unique identifier which is used for searching the correct project. /// - /// Possibly redacted Project object with user and collaborators + /// This method returns possibly redacted Project object with user and collaborators. /// public async Task FindWithUserAndCollaboratorsAsync(int id) { @@ -200,9 +200,9 @@ public async Task FindWithUserAndCollaboratorsAsync(int id) } /// - /// Updates the specified entity excluding the user object. + /// This method updates the specified entity excluding the user object. /// - /// The entity. + /// The entity parameter represents the updated project object. public override void Update(Project entity) { entity = UpdateUpdatedField(entity); @@ -228,11 +228,11 @@ public override void Update(Project entity) } /// - /// Redact user email from the Project if isPublic setting is set to false + /// This method redacts user email from the Project if isPublic setting is set to false. /// - /// The project. + /// The project parameter represents the project object that will be used. /// - /// Project with possibly redacted email depending on setting + /// This method returns the project with possibly redacted email depending on setting. /// private Project RedactUser(Project project) { @@ -246,11 +246,11 @@ private Project RedactUser(Project project) } /// - /// Redact user email from the User if isPublic setting is set to false + /// This method redacts the user email from the User if isPublic setting is set to false. /// - /// The user. + /// The user parameter represents the user object that will be used. /// - /// User with possibly redacted email depending on setting + /// This method returns the user with possibly redacted email depending on setting. /// private User RedactUser(User user) { @@ -263,12 +263,12 @@ private User RedactUser(User user) } /// - /// Redact user email from the Projects in the list. - /// Email will only be redacted if isPublic setting is set to false. + /// This method redacts the user email from the Projects in the list. The email will only be + /// redacted if isPublic setting is set to false. /// - /// The projects. + /// The projects parameter represents the project objects that will be used. /// - /// List of Projects with possibly redacted email depending on setting + /// This method returns a list of Projects with possibly redacted email depending on setting. /// private List RedactUser(List projects) { @@ -280,16 +280,16 @@ private List RedactUser(List projects) } /// - /// Apply query parameters and find project based on these filters + /// This method applies query parameters and find project based on these filters. /// - /// The linq queryable object. - /// The amount of objects to skip. - /// The amount of objects to take. - /// The order by expression. - /// if set to true [order by asc]. - /// Boolean if the project should show highlighted. + /// The linq queryable parameter represents the IQueryable object. + /// The skip parameter represents the number of projects to skip. + /// The take parameter represents the number of projects to return. + /// The order by parameter represents the way how to order the projects. + /// The order by asc parameters represents the order direction (True: asc, False: desc) + /// The highlighted parameter represents the whether to filter highlighted projects. /// - /// IQueryable Projects based on the given filters + /// This method returns a IQueryable Projects collection based on the given filters. /// private IQueryable ApplyFilters( IQueryable queryable, @@ -330,13 +330,13 @@ private IQueryable ApplyFilters( } /// - /// Checks if any of the searchable fields of the project passed contains the provided query. + /// This method checks if any of the searchable fields of the project passed contains the provided query. /// - /// A Project to search in - /// The query to search in the project's searchable fields. + /// The project parameter represents a Project to search in. + /// The query parameter represents the query to search in the project's searchable fields. /// - /// A boolean representing whether or not the passed query was found in the searchable fields of the provided - /// project. + /// This method returns a boolean representing whether or not the passed query was found in the + /// searchable fields of the provided project. /// private static bool ProjectContainsQuery(Project project, string query) { @@ -354,10 +354,10 @@ private static bool ProjectContainsQuery(Project project, string query) } /// - /// Get the project queryable which contains the provided query. + /// This method gets the project queryable which contains the provided query. /// - /// A string to search in the project's fields. - /// The filtered IQueryable including the project user. + /// The query parameter is a string to search in the project's fields. + /// This method returns the filtered IQueryable including the project user. private IQueryable GetProjectQueryable(string query) { return DbSet