diff --git a/.gitignore b/.gitignore index c977dd6..38ee8ff 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ obj/ .vscode/ Migrations/ app.db* -.DS_Store \ No newline at end of file +.DS_Store +app.db +client/node_modules +Scripts/ diff --git a/Controllers/UsersController.cs b/Controllers/AppUsersController.cs similarity index 52% rename from Controllers/UsersController.cs rename to Controllers/AppUsersController.cs index 32ca0f3..d6bbc2b 100644 --- a/Controllers/UsersController.cs +++ b/Controllers/AppUsersController.cs @@ -10,7 +10,7 @@ using TicketManager.Models; namespace TicketManager.Controllers { - [Route("api/[controller]")] + [Route("api/v1/[controller]")] [ApiController] public class UsersController : ControllerBase { @@ -23,22 +23,16 @@ namespace TicketManager.Controllers // GET: api/Users [HttpGet] - public async Task>> GetUsers() + public async Task>> GetUsers() { - return await _context.Users - .Include(p => p.Assignments) - .Include(p => p.Edits) - .ToListAsync(); + return await getAllAppUsersAsync(); } // GET: api/Users/5 [HttpGet("{id}")] - public async Task> GetUser(Guid id) + public async Task> GetUser(Guid id) { - var user = await _context.Users - .Include(p => p.Assignments) - .Include(p => p.Edits) - .FirstOrDefaultAsync(p => p.Id == id); + var user = await getAppUserByIdAsync(id); if (user == null) { @@ -52,7 +46,7 @@ namespace TicketManager.Controllers // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see https://aka.ms/RazorPagesCRUD. [HttpPut("{id}")] - public async Task PutUser(Guid id, User user) + public async Task PutUser(Guid id, AppUser user) { if (id != user.Id) { @@ -84,9 +78,9 @@ namespace TicketManager.Controllers // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see https://aka.ms/RazorPagesCRUD. [HttpPost] - public async Task> PostUser(User user) + public async Task> PostUser(AppUser user) { - _context.Users.Add(user); + _context.AppUsers.Add(user); await _context.SaveChangesAsync(); return CreatedAtAction("GetUser", new { id = user.Id }, user); @@ -94,23 +88,64 @@ namespace TicketManager.Controllers // DELETE: api/Users/5 [HttpDelete("{id}")] - public async Task> DeleteUser(int id) + public async Task> DeleteUser(int id) { - var user = await _context.Users.FindAsync(id); + var user = await _context.AppUsers.FindAsync(id); if (user == null) { return NotFound(); } - _context.Users.Remove(user); + _context.AppUsers.Remove(user); await _context.SaveChangesAsync(); return user; } + [HttpGet("{id}/projects")] + public async Task>> GetAppUserProjects(Guid id) + { + AppUser user = await getAppUserByIdAsync(id); + if (user == null) + { + return BadRequest(); + } + return user.GetProjects(); + } + + [HttpGet("{id}/tickets/")] + public async Task>> GetAppUserTickets(Guid id) + { + AppUser user = await getAppUserByIdAsync(id); + if (user == null) + { + return BadRequest(); + } + return user.GetTickets(); + } + private bool UserExists(Guid id) { - return _context.Users.Any(e => e.Id == id); + return _context.AppUsers.Any(e => e.Id == id); + } + + private IQueryable appUserQuery() + { + return _context.AppUsers + .Include(p => p.Assignments) + .ThenInclude(a => a.Project) + .ThenInclude(p => p.Tickets) + .Include(p => p.Edits); + } + + private async Task>> getAllAppUsersAsync() + { + return await appUserQuery().ToListAsync(); + } + + private async Task getAppUserByIdAsync(Guid id) + { + return await appUserQuery().FirstOrDefaultAsync(a => a.Id == id); } } } diff --git a/Controllers/FilesController.cs b/Controllers/FilesController.cs index 22aa086..6481b76 100644 --- a/Controllers/FilesController.cs +++ b/Controllers/FilesController.cs @@ -10,7 +10,7 @@ using TicketManager.Models; namespace TicketManager.Controllers { - [Route("api/[controller]")] + [Route("api/v1/[controller]")] [ApiController] public class FilesController : ControllerBase { diff --git a/Controllers/HistoriesController.cs b/Controllers/HistoriesController.cs index 85c8d89..0934909 100644 --- a/Controllers/HistoriesController.cs +++ b/Controllers/HistoriesController.cs @@ -10,7 +10,7 @@ using TicketManager.Models; namespace TicketManager.Controllers { - [Route("api/[controller]")] + [Route("api/v1/[controller]")] [ApiController] public class HistoriesController : ControllerBase { diff --git a/Controllers/NotesController.cs b/Controllers/NotesController.cs index 0646e83..f2a537b 100644 --- a/Controllers/NotesController.cs +++ b/Controllers/NotesController.cs @@ -10,7 +10,7 @@ using TicketManager.Models; namespace TicketManager.Controllers { - [Route("api/[controller]")] + [Route("api/v1/[controller]")] [ApiController] public class NotesController : ControllerBase { diff --git a/Controllers/ProjectsController.cs b/Controllers/ProjectsController.cs index 290d39d..5e6ffca 100644 --- a/Controllers/ProjectsController.cs +++ b/Controllers/ProjectsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Mime; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -8,9 +9,11 @@ using Microsoft.EntityFrameworkCore; using TicketManager.Data; using TicketManager.Models; + namespace TicketManager.Controllers { - [Route("api/[controller]")] + [Produces("application/json")] + [Route("api/v1/[controller]")] [ApiController] public class ProjectsController : ControllerBase { @@ -21,59 +24,82 @@ namespace TicketManager.Controllers _context = context; } - // GET: api/Projects + /// + /// Returns all existing projects. + /// + /// + /// Sample request: + /// + /// GET: api/Projects + /// + /// + /// Returns all existing projects [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetProjects() { - return await _context.Projects - .Include(p => p.Assignments) - .ThenInclude(a => a.User) - .Include(p => p.Tickets) - .Include(p => p.Manager) - .Include(p => p.Files) - .AsNoTracking() - .ToListAsync(); + return await GetAllProjectsAsync(); } - // GET: api/Projects/5 + /// + /// Returns a specific project. + /// + /// + /// Sample request: + /// + /// GET: api/Projects/2 + /// + /// + /// Returns a specific project + /// If the required project is null [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetProject(int id) { - var project = await _context.Projects - .Include(p => p.Assignments) - .Include(p => p.Tickets) - .Include(p => p.Manager) - .Include(p => p.Files) - .AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == id); - - + Project project = await GetProjectByIdAsync(id); if (project == null) - { return NotFound(); } - + { + return NotFound(); + } return project; } - // GET: api/Projects/5/Members - [HttpGet("{id}/members")] - public async Task>> GetProjectMembers(int id) - { - var project = await _context.Projects.FindAsync(id); - if (project == null) - { return NotFound(); } - - return project.GetMembers(); - } // PUT: api/Projects/5 // To protect from overposting attacks, please enable the specific properties you want to bind to, for // more details see https://aka.ms/RazorPagesCRUD. + /// + /// Updates a specific project. + /// + /// + /// Sample request: + /// + /// PUT: api/Projects/3 + /// { + /// "id": "357727fd-5262-4522-b8a3-38271d43de84", + /// "firstName": "Thomas", + /// "lastName": "Price", + /// "presentation": "New Team?!", + /// "email": "tp@mail.com", + /// "phone": "0198237645" + /// } + /// + /// + /// Returns the modified project + /// Request was succesful but no content is changed + /// If the required project is null [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task PutProject(int id, Project project) { if (id != project.Id) - { return BadRequest(); } + { + return BadRequest(); + } _context.Entry(project).State = EntityState.Modified; @@ -109,7 +135,7 @@ namespace TicketManager.Controllers } [HttpPost("{id}/addmembers")] - public async Task> PostAssignment(int id, List usersToAdd) + public async Task> PostAssignment(int id, List usersToAdd) { var project = await _context.Projects.FindAsync(id); @@ -157,6 +183,76 @@ namespace TicketManager.Controllers return project; } + // GET: api/Projects/5/Members + [HttpGet("{id}/members")] + public async Task>> GetProjectMembers(int id) + { + Project project = await GetProjectByIdAsync(id); + if (project == null) + { return NotFound(); } + return project.GetMembers(); + } + + [HttpPut("{id}/members")] + public async Task> SetProjectMembers(int id, List projectMembers) + { + Project project = await GetProjectByIdAsync(id); + project.SetMembers(projectMembers); + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateException /* ex */) + { + //Log the error (uncomment ex variable name and write a log.) + ModelState.AddModelError("", "Unable to save changes. " + + "Try again, and if the problem persists, " + + "see your system administrator."); + } + return NoContent(); + } + + [HttpPut("{id}/addMembers")] + public async Task> AddMembersToProject(int id, List usersToAdd) + { + if (usersToAdd == null) + { + return BadRequest(); + } + Project project = await GetProjectByIdAsync(id); + project.AddMembers(usersToAdd); + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateException /* ex */) + { + //Log the error (uncomment ex variable name and write a log.) + ModelState.AddModelError("", "Unable to save changes. " + + "Try again, and if the problem persists, " + + "see your system administrator."); + } + return NoContent(); + } + + [HttpPut("{id}/removeMembers")] + public async Task> RemoveMembersFromProject(int id, List usersToRemove) + { + Project project = await GetProjectByIdAsync(id); + project.RemoveMembers(usersToRemove); + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateException /* ex */) + { + //Log the error (uncomment ex variable name and write a log.) + ModelState.AddModelError("", "Unable to save changes. " + + "Try again, and if the problem persists, " + + "see your system administrator."); + } + return NoContent(); + } private bool ProjectExists(int id) { @@ -166,5 +262,26 @@ namespace TicketManager.Controllers { return _context.Assignments.Any(e => e.ProjectId == id); } + + private async Task>> GetAllProjectsAsync() + { + return await makeProjectsQueryAsync() + .ToListAsync(); + } + private async Task GetProjectByIdAsync(int id) + { + return await makeProjectsQueryAsync() + .FirstOrDefaultAsync(p => p.Id == id); + } + + private IQueryable makeProjectsQueryAsync() + { + return _context.Projects + .Include(p => p.Assignments) + .ThenInclude(a => a.User) + .Include(p => p.Tickets) + .Include(p => p.Manager) + .Include(p => p.Files); + } } } diff --git a/Controllers/TicketsController.cs b/Controllers/TicketsController.cs index bddebe9..c61ce5b 100644 --- a/Controllers/TicketsController.cs +++ b/Controllers/TicketsController.cs @@ -10,7 +10,7 @@ using TicketManager.Models; namespace TicketManager.Controllers { - [Route("api/[controller]")] + [Route("api/v1/[controller]")] [ApiController] public class TicketsController : ControllerBase { @@ -25,25 +25,14 @@ namespace TicketManager.Controllers [HttpGet] public async Task>> GetTickets() { - return await _context.Tickets - .Include(t => t.Creator) - .Include(p => p.Notes) - .Include(p => p.Edits) - .Include(p => p.Files) - .AsNoTracking() - .ToListAsync(); + return await getAllTicketsAsync(); } // GET: api/Tickets/5 [HttpGet("{id}")] public async Task> GetTicket(int id) { - var ticket = await _context.Tickets - .Include(t => t.Creator) - .Include(p => p.Notes) - .Include(p => p.Edits) - .Include(p => p.Files) - .FirstOrDefaultAsync(t => t.Id == id); + var ticket = await getTicketByIdAsync(id); if (ticket == null) { @@ -113,9 +102,47 @@ namespace TicketManager.Controllers return ticket; } + [HttpGet("{id}/assignees")] + public async Task>> GetTicketAssignees(int id) + { + Ticket ticket = await getTicketByIdAsync(id); + return ticket.GetAssignees(); + } + + [HttpPut("{id}/closed")] + public async Task CloseTicket(int id) + { + Ticket ticket = await getTicketByIdAsync(id); + ticket.Close(); + return NoContent(); + } + private bool TicketExists(int id) { return _context.Tickets.Any(e => e.Id == id); } + + private IQueryable ticketQuery() // problem with link + { + return _context.Tickets + .Include(p => p.Project) + .ThenInclude(a => a.Assignments) + .ThenInclude(p => p.User) + // .Include(p => p.Edits) + // .Include(p => p.Notes) + // .Include(p => p.Files) + .Include(p => p.Creator) + ; + } + + private async Task>> getAllTicketsAsync() + { + return await ticketQuery().ToListAsync(); + } + + private async Task getTicketByIdAsync(int id) + { + return await ticketQuery().FirstOrDefaultAsync(a => a.Id == id); + } } } diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index c615c4f..b249c6b 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -9,7 +9,7 @@ namespace TicketManager.Data { } public DbSet Projects { get; set; } - public DbSet Users { get; set; } + public DbSet AppUsers { get; set; } public DbSet Tickets { get; set; } public DbSet Assignments { get; set; } public DbSet Edits { get; set; } @@ -20,8 +20,6 @@ namespace TicketManager.Data { base.OnModelCreating(builder); builder.Entity().HasKey(a => new { a.ProjectId, a.UserId }); - builder.Entity().HasOne(a => a.Project).WithMany(p => p.Assignments).HasForeignKey(a => a.ProjectId); - builder.Entity().HasOne(a => a.User).WithMany(u => u.Assignments).HasForeignKey(a => a.UserId); } } } \ No newline at end of file diff --git a/Models/AppUser.cs b/Models/AppUser.cs new file mode 100644 index 0000000..fb53b1a --- /dev/null +++ b/Models/AppUser.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; + +namespace TicketManager.Models +{ + public class AppUser + { + public Guid Id { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "First Name")] + public string FirstName { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "Last Name")] + public string LastName { get; set; } + + [Display(Name = "Full Name")] + public string FullName => $"{FirstName} {LastName}"; + + [StringLength(200)] + [Display(Name = "Bio")] + public string Presentation { get; set; } + + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } + + [DataType(DataType.Date)] + [Display(Name = "Member since"), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")] + public DateTime Created_at { get; private set; } = DateTime.Now; + + // [Display(Name = "Avatar")] + // public byte[] Picture { get; set; } + // public Role Role { get; set; } + + public List Assignments { get; set; } = new List(); + + [Display(Name = "Activity")] + public List Edits { get; set; } = new List(); + + // Methods + public List GetProjects() + { + return Assignments.Select(a => a.Project).ToList(); + } + + // public Project GetProject(int id) + // { + // return Assignments.Single(a => a.Project.Id == id).Project; + // } + + // public List GetProjectMembers(int id) + // { + // return GetProject(id).GetMembers(); + // } + public List GetTickets() + { + List tickets = new List(); + GetProjects().ForEach(p => tickets.Concat(p.Tickets)); + return tickets; + } + } +} \ No newline at end of file diff --git a/Models/Assignment.cs b/Models/Assignment.cs index 02aaa9c..4d3c496 100644 --- a/Models/Assignment.cs +++ b/Models/Assignment.cs @@ -4,8 +4,8 @@ namespace TicketManager.Models { public class Assignment { - public int Id { get; set; } - public User User { get; set; } + // public int Id { get; set; } + public AppUser User { get; set; } public Guid UserId { get; set; } public Project Project { get; set; } public int ProjectId { get; set; } diff --git a/Models/File.cs b/Models/File.cs index 8a7d885..8dd01e4 100644 --- a/Models/File.cs +++ b/Models/File.cs @@ -5,13 +5,27 @@ namespace TicketManager.Models { public class File { - public int Id { get; set; } - public string Location { get; set; } - public string Description { get; set; } - public int Size { get; set; } - public string Format { get; set; } - public User AddedBy { get; set; } + + public int Id { get; set; } + + public string FileName { get; set; } + + private string _location; + public string Location + { + get { return _location; } + private set + { + string filesUrl = ""; + _location = $"{filesUrl}/{FileName}"; + } + } + public string Description { get; set; } + public int Size { get; set; } // deduce auto from FileName + public string Format { get; set; } // deduce auto from FileName + + public AppUser AddedBy { get; set; } public int UserId { get; set; } // public ITask AddedTo { get; set; } // public int ITaskId { get; set; } diff --git a/Models/History.cs b/Models/History.cs index 44d45cd..e039343 100644 --- a/Models/History.cs +++ b/Models/History.cs @@ -6,12 +6,10 @@ namespace TicketManager.Models public class History { public int Id { get; set; } - public string Title { get; set; } public string Description { get; set; } - public DateTime UpdateDate { get; } = DateTime.Now; + public DateTime UpdateDate { get; set; } = DateTime.Now; public ActivityType ActivityType { get; set; } = ActivityType.Undefined; - - public User User { get; set; } + public AppUser User { get; set; } public int UserId { get; set; } } } \ No newline at end of file diff --git a/Models/Interfaces/ITask.cs b/Models/Interfaces/ITask.cs index 9311efe..124b8c0 100644 --- a/Models/Interfaces/ITask.cs +++ b/Models/Interfaces/ITask.cs @@ -1,13 +1,28 @@ using System; +using System.Collections.Generic; namespace TicketManager.Models { - public interface ITask + // public interface ITask + public abstract class ITask { int Id { get; set; } string Title { get; set; } string Description { get; set; } DateTime CreatedAt { get; } DateTime PlannedEnding { get; set; } + List Edits { get; set; } + + public virtual void AddLogEntry(string description)//, User user) + { + History Edit = new History() + { + Description = description, + ActivityType = ActivityType.Undefined, + // User = user, + UpdateDate = DateTime.Now + }; + Edits.Add(Edit); + } } } diff --git a/Models/Note.cs b/Models/Note.cs index 7b6834f..84d5c05 100644 --- a/Models/Note.cs +++ b/Models/Note.cs @@ -8,7 +8,7 @@ namespace TicketManager.Models public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } - public DateTime Created_at { get; } = DateTime.Now; + public DateTime Created_at { get; set; } = DateTime.Now; public Ticket Ticket { get; set; } } diff --git a/Models/Project.cs b/Models/Project.cs index b1875b8..4f49bd0 100644 --- a/Models/Project.cs +++ b/Models/Project.cs @@ -1,66 +1,68 @@ using System; using System.Collections.Generic; using System.Linq; +using System.ComponentModel.DataAnnotations; namespace TicketManager.Models { public class Project : ITask { public int Id { get; set; } + + [Required] + [StringLength(50)] + [Display(Name = "Title")] public string Title { get; set; } + + [StringLength(200)] + [Display(Name = "Short Description")] public string Description { get; set; } - public DateTime CreatedAt { get; } = DateTime.Now; + + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = false)] + public DateTime CreatedAt { get; private set; } = DateTime.Now; + + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] public DateTime PlannedEnding { get; set; } - public float Progression + + private decimal _progression; + [Display(Name = "Progress")] + public decimal Progression { get { - return this.Tickets.Count() == 0 ? 0 : (float)this.Tickets. + return _progression; + } + private set + { + _progression = Tickets.Count() == 0 ? 0 : + (decimal)this.Tickets. Where(t => t.Status == Status.Done).Count() - / this.Tickets.Count() - * 100; + / this.Tickets.Count() * 100; } } + + [Display(Name = "Project Status")] public Status Status { get; set; } = Status.ToDo; - public User Manager { get; set; } - public Guid ManagerId { get; set; } - private List _assignments; - public List Assignments - { - get - { return _assignments ?? new List(); } - set - { _assignments = value; } - } - private List _tickets; - public List Tickets - { - get - { return _tickets ?? new List(); } - set { _tickets = value; } - } - // private List _edits; - // public List Edits - // { - // get - // { return _edits ?? new List(); } - // set { _edits = value; } - // } - private List _files; - public List Files - { - get - { return _files ?? new List(); } - set { _files = value; } - } + [Display(Name = "Project Manager")] + public AppUser Manager { get; set; } + + public List Assignments { get; set; } = new List(); + + public List Tickets { get; set; } = new List(); + + public List Edits { get; set; } = new List(); + + public List Files { get; set; } = new List(); // Methods - public List GetMembers() + public List GetMembers() { return this.Assignments.Select(a => a.User).ToList(); } - public void AddMembers(List usersToAdd) + public void AddMembers(List usersToAdd) { var projectUsers = new List (this.Assignments.Select(a => a.UserId)); @@ -76,13 +78,37 @@ namespace TicketManager.Models UserId = user.Id }; this.Assignments.Add(newAssign); + // AddLogEntry(this, " joined the project."); } // return this.Assignments; } - public void RemoveMembers(List membersToRemove) + public void RemoveMembers(List membersToRemove) { this.Assignments.RemoveAll(a => membersToRemove.Contains(a.User)); } + + public void SetMembers(List projectMembers) + { + var currentProjectMembers = this.GetMembers(); + if (currentProjectMembers != null) + { + var membersToRemove = currentProjectMembers + .FindAll( + cp => !projectMembers.Contains(cp) + ); + this.RemoveMembers(membersToRemove); + + var membersToAdd = projectMembers.FindAll( + pm => !currentProjectMembers.Contains(pm) + ); + this.AddMembers(membersToAdd); + } + else + { + this.AddMembers(projectMembers); + } + + } public int GetMembersCount() => this.GetMembers().Count(); public void GetTicketsCount() => this.Tickets.Count(); public void GetTicketsUpdates() @@ -91,5 +117,17 @@ namespace TicketManager.Models { this.Status = Status.Done; } + + // private void AddLogEntry(string description)//, User user) + // { + // History Edit = new History() + // { + // Description = description, + // ActivityType = ActivityType.Undefined, + // // User = user, + // UpdateDate = DateTime.Now + // }; + // this.Edits.Add(Edit); + // } } } \ No newline at end of file diff --git a/Models/Ticket.cs b/Models/Ticket.cs index 727ca6f..29c5e01 100644 --- a/Models/Ticket.cs +++ b/Models/Ticket.cs @@ -1,14 +1,27 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; namespace TicketManager.Models { public class Ticket : ITask { public int Id { get; set; } + + [Required] + [StringLength(100)] public string Title { get; set; } + + [StringLength(100)] public string Description { get; set; } - public DateTime CreatedAt { get; } = DateTime.Now; + + [DataType(DataType.Date)] + [Display(Name = "Creation Date"), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")] + public DateTime CreatedAt { get; private set; } = DateTime.Now; + + [DataType(DataType.Date)] + [Display(Name = "Estimated Ending Date"), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")] public DateTime PlannedEnding { get; set; } public Status Status { get; set; } = Status.ToDo; @@ -16,42 +29,28 @@ namespace TicketManager.Models public Difficulty Difficulty { get; set; } = Difficulty.Undefined; public Category Category { get; set; } = Category.Undefined; - public User Creator { get; set; } + [Display(Name = "Created By")] + public AppUser Creator { get; set; } public Guid CreatorId { get; set; } - // public Project Project { get; set; } - // public int ProjectId { get; set; } - private List _notes; - public List Notes - { - get - { - return _notes ?? new List(); - } - set { _notes = value; } - } - private List _edits; - public List Edits - { - get - { - return _edits ?? new List(); - } - set { _edits = value; } - } - private List _files; - public List Files - { - get - { - return _files ?? new List(); - } - set { _files = value; } - } + + [Display(Name = "Project")] + public Project Project { get; set; } + public int ProjectId { get; set; } + public List Notes = new List(); + + public List Edits = new List(); + + public List Files = new List(); // Methods - public void GetAssignees() { throw new NotImplementedException("Not Implemented"); } + public List GetAssignees() + { + return Project.Assignments.Select(a => a.User).ToList(); + } public void GetLastUpdateTime() { throw new NotImplementedException("Not Implemented"); } - public void Close() { throw new NotImplementedException("Not Implemented"); } - public void AddFile() { throw new NotImplementedException("Not Implemented"); } + public void Close() + { + Status = Status.Done; + } } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index b3c5cf8..35c5cb4 100644 --- a/Program.cs +++ b/Program.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; namespace TicketManager { +#pragma warning disable CS1591 public class Program { public static void Main(string[] args) @@ -23,4 +24,5 @@ namespace TicketManager webBuilder.UseStartup(); }); } +#pragma warning restore CS1591 } diff --git a/README.md b/README.md index f014711..3bdfd35 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,42 @@ - [Follow the link](https://docs.google.com/presentation/d/1Gunf5MRJ_KcoFwo0x_vV8YVHnf9l0V8n7BiJGz6p4cI/edit?usp=sharing) -## ToDo +## API Documentation +### v1 + +- [Internal Link. Don't forget to update](https://localhost:5001/swagger) + +## Features + +## Supports + +- Web +- Progressive Web App +- Mobile + +## Technical Stack + +- `React` client on the front-end (TypeScript) +- [Materialize](https://materializecss.com) CSS librairy for styling +- API: Newtonsoft.Json, to avoid cycle errors +- Hosting: ? +- Authentication : [Auth0](https://auth0.com/) +- Analytics : Google Analytics & Mixpanel + +## Versions + +### Features in v.0.1 + +## TO DO + +- Write API tests using Postman: request + test, environment variables, mock server +- Annotate API request in controllers +- Annotate Properties in Models - Write backend tests - Have a Look at typeahead component +- Ensure Tickets Edits belong to Project Edits +- Ensure Tickets Files belong to Project Files +- Write a query class to refactor code and optimize perf on get queries (AsNoTracking) +- Async model methods ? +- setMembers & removeMembers from project api not working diff --git a/Startup.cs b/Startup.cs index 9da91b4..b169da0 100644 --- a/Startup.cs +++ b/Startup.cs @@ -17,7 +17,10 @@ using System.Reflection; using System.IO; using TicketManager.Data; using TicketManager.Models; +using Microsoft.AspNetCore.Mvc.NewtonsoftJson; +using Newtonsoft.Json; +[assembly: ApiController] namespace TicketManager { public class Startup @@ -34,7 +37,12 @@ namespace TicketManager { services.AddDbContext(options => options.UseSqlite(Configuration.GetConnectionString("Sqlite"))); - services.AddControllers(); + services.AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; // avoid cycle ref errors + }); + services.AddSpaStaticFiles(configuration => { configuration.RootPath = "client/build"; @@ -45,7 +53,7 @@ namespace TicketManager { Version = "v1", Title = "Ticket Manager API", - Description = "A simple example ASP.NET Core Web API", + Description = "Ticket Manger API for Teams", Contact = new OpenApiContact { Name = "Ruidy Nemausat", @@ -53,12 +61,12 @@ namespace TicketManager Url = new Uri("https://ruidywebsite.herokuapp.com/"), } }); - // Set the comments path for the Swagger JSON and UI. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); }); + services.AddSwaggerGenNewtonsoftSupport(); // explicit opt-in - needs to be placed after AddSwaggerGen() } @@ -70,19 +78,23 @@ namespace TicketManager { app.UseDeveloperExceptionPage(); } + else + { + app.UseHsts(); + } - + app.UseHttpsRedirection(); app.UseDefaultFiles(); - app.UseStaticFiles(); + app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ticket Manager API V1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ticket Manager API v1"); }); - app.UseHttpsRedirection(); + app.UseSpaStaticFiles(); app.UseRouting(); diff --git a/TicketManager.csproj b/TicketManager.csproj index ae2c5af..79dbdef 100644 --- a/TicketManager.csproj +++ b/TicketManager.csproj @@ -16,6 +16,7 @@ + @@ -27,14 +28,10 @@ - - + + - - - - \ No newline at end of file diff --git a/appsettings.json b/appsettings.json index 51642cd..390f10c 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,4 +1,5 @@ { + "https_port": 443, "Logging": { "LogLevel": { "Default": "Information",