From 1a30145b5088b96dd4b31dee8c795492e38a0fae Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 21 Apr 2026 15:28:58 -0400 Subject: [PATCH 1/2] feat: Add Project Publications --- src/Analysim.Core/Entities/Project.cs | 1 + src/Analysim.Core/Entities/Publication.cs | 30 + .../Data/ApplicationDbContext.cs | 9 + .../ApplicationDbContextModelSnapshot.cs | 56 +- ...20260421174128_AddPublications.Designer.cs | 988 ++++++++++++++++++ .../20260421174128_AddPublications.cs | 92 ++ .../src/app/interfaces/publication.ts | 9 + .../modal-delete-publication.component.html | 44 + .../modal-delete-publication.component.scss | 15 + ...modal-delete-publication.component.spec.ts | 23 + .../modal-delete-publication.component.ts | 47 + .../modal-edit-publication.component.html | 94 ++ .../modal-edit-publication.component.scss | 58 + .../modal-edit-publication.component.spec.ts | 23 + .../modal-edit-publication.component.ts | 94 ++ .../project-publication.component.html | 78 +- .../project-publication.component.scss | 9 +- .../project-publication.component.ts | 52 +- .../publication-item.component.html | 44 + .../publication-item.component.scss | 18 + .../publication-item.component.spec.ts | 23 + .../publication-item.component.ts | 50 + .../projects/project/project.component.html | 38 +- .../src/app/projects/projects.module.ts | 6 + .../src/app/services/project.service.ts | 54 + .../Controllers/ProjectController.cs | 121 +++ .../ViewModels/Project/CreatePublicationVM.cs | 17 + .../ViewModels/Project/PublicationVM.cs | 17 + 28 files changed, 2050 insertions(+), 60 deletions(-) create mode 100644 src/Analysim.Core/Entities/Publication.cs create mode 100644 src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.Designer.cs create mode 100644 src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.cs create mode 100644 src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.spec.ts create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.spec.ts create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.html create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.scss create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.spec.ts create mode 100644 src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.ts create mode 100644 src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs create mode 100644 src/Analysim.Web/ViewModels/Project/PublicationVM.cs diff --git a/src/Analysim.Core/Entities/Project.cs b/src/Analysim.Core/Entities/Project.cs index b09c2d51..a231de1a 100644 --- a/src/Analysim.Core/Entities/Project.cs +++ b/src/Analysim.Core/Entities/Project.cs @@ -42,6 +42,7 @@ public class Project // Comments public ICollection ProjectComments { get; set; } = new List(); + public ICollection Publications { get; set; } = new List(); public int ForkedFromProjectID { get; set; } } diff --git a/src/Analysim.Core/Entities/Publication.cs b/src/Analysim.Core/Entities/Publication.cs new file mode 100644 index 00000000..bdca5644 --- /dev/null +++ b/src/Analysim.Core/Entities/Publication.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Core.Entities +{ + public class Publication + { + // PK + [KeyAttribute] + public int PublicationID { get; set; } + + // Project publication is linked to + [ForeignKey("Project")] + public int ProjectID { get; set; } + public Project Project{ get; set; } = null!; + + // publication content + [Required(ErrorMessage = "Publication Title is a required field.")] + public string Title { get; set; } = string.Empty; + public string? Url { get; set; } + public string? Doi { get; set; } + public string? SourceAuthor { get; set; } + public int? Year { get; set; } + + // Timestamps + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs b/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs index 39ecfe46..ca5d69b3 100644 --- a/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs +++ b/src/Analysim.Infrastructure/Data/ApplicationDbContext.cs @@ -182,6 +182,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasIndex(cf => cf.UserID); #endregion + + // One To Many Relationship (Project -> Publication) + modelBuilder.Entity() + .HasMany(p => p.Publications) + .WithOne(pp => pp.Project) + .HasForeignKey(pp => pp.ProjectID) + .OnDelete(DeleteBehavior.Cascade); } @@ -200,5 +207,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet ProjectComments { get; set; } public DbSet ProjectCommentLikes { get; set; } public DbSet ProjectCommentFlags { get; set; } + + public DbSet Publications { get; set; } } } diff --git a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index b9654607..fc110a57 100644 --- a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -374,6 +374,43 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ProjectUsers"); }); + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.Property("PublicationID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PublicationID")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Doi") + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("SourceAuthor") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("PublicationID"); + + b.HasIndex("ProjectID"); + + b.ToTable("Publications"); + }); + modelBuilder.Entity("Core.Entities.Tag", b => { b.Property("TagID") @@ -519,21 +556,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 1, - ConcurrencyStamp = "45fc9c92-801d-433f-ab00-280c2334bc89", + ConcurrencyStamp = "e2465fb1-9027-4762-9172-0ddfe8ba46ff", Name = "Admin", NormalizedName = "ADMIN" }, new { Id = 2, - ConcurrencyStamp = "922f6db8-d335-4aa4-9331-12097412cf5e", + ConcurrencyStamp = "d9b0d06b-709c-4ee9-82c5-e733494e0a9a", Name = "Customer", NormalizedName = "CUSTOMER" }, new { Id = 3, - ConcurrencyStamp = "cecf34c5-c9e0-4b99-9fb7-1441045209d8", + ConcurrencyStamp = "fdf254aa-d0e4-4a22-ad0c-b3fb12cb69c7", Name = "Moderator", NormalizedName = "MODERATOR" }); @@ -805,6 +842,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Publications") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + modelBuilder.Entity("Core.Entities.UserUser", b => { b.HasOne("Core.Entities.User", "Follower") @@ -898,6 +946,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ProjectTags"); b.Navigation("ProjectUsers"); + + b.Navigation("Publications"); }); modelBuilder.Entity("Core.Entities.ProjectComment", b => diff --git a/src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.Designer.cs b/src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.Designer.cs new file mode 100644 index 00000000..bb2baee3 --- /dev/null +++ b/src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.Designer.cs @@ -0,0 +1,988 @@ +// +using System; +using Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260421174128_AddPublications")] + partial class AddPublications + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Analysim.Core.Entities.BlobFileContent", b => + { + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BlobFileID"); + + b.ToTable("BlobFileContent"); + }); + + modelBuilder.Entity("Analysim.Core.Entities.NotebookContent", b => + { + b.Property("NotebookID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("NotebookID", "Version"); + + b.ToTable("NotebookContent"); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.Property("BlobFileID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BlobFileID")); + + b.Property("Container") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Directory") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("BlobFileID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID"); + + b.ToTable("BlobFiles"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.Property("NotebookID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NotebookID")); + + b.Property("Container") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Directory") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("Route") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("text"); + + b.Property("type") + .HasColumnType("text"); + + b.HasKey("NotebookID"); + + b.HasIndex("Directory"); + + b.HasIndex("ProjectID"); + + b.ToTable("Notebook"); + }); + + modelBuilder.Entity("Core.Entities.ObservableNotebookDataset", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); + + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("NotebookID") + .HasColumnType("integer"); + + b.Property("datasetName") + .HasColumnType("text"); + + b.Property("datasetURL") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("NotebookID"); + + b.ToTable("ObservableNotebookDataset"); + }); + + modelBuilder.Entity("Core.Entities.Project", b => + { + b.Property("ProjectID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectID")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ForkedFromProjectID") + .HasColumnType("integer"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Route") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visibility") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ProjectID"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.Property("CommentID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CommentID")); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsPendingReview") + .HasColumnType("boolean"); + + b.Property("ParentCommentID") + .HasColumnType("integer"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("CommentID"); + + b.HasIndex("ParentCommentID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectComments"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentFlag", b => + { + b.Property("FlagID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FlagID")); + + b.Property("CommentContentSnapshot") + .HasColumnType("text"); + + b.Property("CommentID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("FlagID"); + + b.HasIndex("UserID"); + + b.HasIndex("CommentID", "UserID") + .IsUnique(); + + b.ToTable("ProjectCommentFlags"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentLike", b => + { + b.Property("CommentID") + .HasColumnType("integer"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CommentID", "UserID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectCommentLikes"); + }); + + modelBuilder.Entity("Core.Entities.ProjectTag", b => + { + b.Property("ProjectID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("TagID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.HasKey("ProjectID", "TagID"); + + b.HasIndex("TagID"); + + b.ToTable("ProjectTags"); + }); + + modelBuilder.Entity("Core.Entities.ProjectUser", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("ProjectID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("IsFollowing") + .HasColumnType("boolean"); + + b.Property("UserRole") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserID", "ProjectID"); + + b.HasIndex("ProjectID"); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.Property("PublicationID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PublicationID")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Doi") + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("SourceAuthor") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("PublicationID"); + + b.HasIndex("ProjectID"); + + b.ToTable("Publications"); + }); + + modelBuilder.Entity("Core.Entities.Tag", b => + { + b.Property("TagID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TagID")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("TagID"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastOnline") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("RegistrationSurvey") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Core.Entities.UserUser", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("FollowerID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.HasKey("UserID", "FollowerID"); + + b.HasIndex("FollowerID"); + + b.ToTable("UserUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = 1, + ConcurrencyStamp = "e2465fb1-9027-4762-9172-0ddfe8ba46ff", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = 2, + ConcurrencyStamp = "d9b0d06b-709c-4ee9-82c5-e733494e0a9a", + Name = "Customer", + NormalizedName = "CUSTOMER" + }, + new + { + Id = 3, + ConcurrencyStamp = "fdf254aa-d0e4-4a22-ad0c-b3fb12cb69c7", + Name = "Moderator", + NormalizedName = "MODERATOR" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Analysim.Core.Entities.BlobFileContent", b => + { + b.HasOne("Core.Entities.BlobFile", "BlobFile") + .WithMany("BlobFileContents") + .HasForeignKey("BlobFileID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlobFile"); + }); + + modelBuilder.Entity("Analysim.Core.Entities.NotebookContent", b => + { + b.HasOne("Core.Entities.Notebook", "Notebook") + .WithMany("NotebookContents") + .HasForeignKey("NotebookID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notebook"); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("BlobFiles") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Entities.User", "User") + .WithMany("BlobFiles") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Notebooks") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Core.Entities.ObservableNotebookDataset", b => + { + b.HasOne("Core.Entities.Notebook", "notebook") + .WithMany("observableNotebookDatasets") + .HasForeignKey("NotebookID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("notebook"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.HasOne("Core.Entities.ProjectComment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentID") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectComments") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectComments") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParentComment"); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentFlag", b => + { + b.HasOne("Core.Entities.ProjectComment", "ProjectComment") + .WithMany("CommentFlags") + .HasForeignKey("CommentID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("CommentFlags") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectComment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentLike", b => + { + b.HasOne("Core.Entities.ProjectComment", "ProjectComment") + .WithMany("CommentLikes") + .HasForeignKey("CommentID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectComment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectTag", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectTags") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Tag", "Tag") + .WithMany("ProjectTags") + .HasForeignKey("TagID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Core.Entities.ProjectUser", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectUsers") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectUsers") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Publications") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Core.Entities.UserUser", b => + { + b.HasOne("Core.Entities.User", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("Followers") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Follower"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.Navigation("BlobFileContents"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.Navigation("NotebookContents"); + + b.Navigation("observableNotebookDatasets"); + }); + + modelBuilder.Entity("Core.Entities.Project", b => + { + b.Navigation("BlobFiles"); + + b.Navigation("Notebooks"); + + b.Navigation("ProjectComments"); + + b.Navigation("ProjectTags"); + + b.Navigation("ProjectUsers"); + + b.Navigation("Publications"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.Navigation("CommentFlags"); + + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("Core.Entities.Tag", b => + { + b.Navigation("ProjectTags"); + }); + + modelBuilder.Entity("Core.Entities.User", b => + { + b.Navigation("BlobFiles"); + + b.Navigation("CommentFlags"); + + b.Navigation("CommentLikes"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("ProjectComments"); + + b.Navigation("ProjectUsers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.cs b/src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.cs new file mode 100644 index 00000000..b270d71f --- /dev/null +++ b/src/Analysim.Infrastructure/Migrations/20260421174128_AddPublications.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + public partial class AddPublications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Publications", + columns: table => new + { + PublicationID = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProjectID = table.Column(type: "integer", nullable: false), + Title = table.Column(type: "text", nullable: false), + Url = table.Column(type: "text", nullable: true), + Doi = table.Column(type: "text", nullable: true), + SourceAuthor = table.Column(type: "text", nullable: true), + Year = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Publications", x => x.PublicationID); + table.ForeignKey( + name: "FK_Publications_Projects_ProjectID", + column: x => x.ProjectID, + principalTable: "Projects", + principalColumn: "ProjectID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 1, + column: "ConcurrencyStamp", + value: "e2465fb1-9027-4762-9172-0ddfe8ba46ff"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 2, + column: "ConcurrencyStamp", + value: "d9b0d06b-709c-4ee9-82c5-e733494e0a9a"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 3, + column: "ConcurrencyStamp", + value: "fdf254aa-d0e4-4a22-ad0c-b3fb12cb69c7"); + + migrationBuilder.CreateIndex( + name: "IX_Publications_ProjectID", + table: "Publications", + column: "ProjectID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Publications"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 1, + column: "ConcurrencyStamp", + value: "45fc9c92-801d-433f-ab00-280c2334bc89"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 2, + column: "ConcurrencyStamp", + value: "922f6db8-d335-4aa4-9331-12097412cf5e"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 3, + column: "ConcurrencyStamp", + value: "cecf34c5-c9e0-4b99-9fb7-1441045209d8"); + } + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts b/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts new file mode 100644 index 00000000..f33c3a8a --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts @@ -0,0 +1,9 @@ +export interface Publication { + publicationID: number; + title: string; + url: string; + doi: string; + sourceAuthor: string; + year: number; + createdAt: string; +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html new file mode 100644 index 00000000..82cbb4fd --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html @@ -0,0 +1,44 @@ + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss new file mode 100644 index 00000000..e4caf7ff --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss @@ -0,0 +1,15 @@ +.modal-confirm{ + text-align: center; + font-weight: 700; + font-size: large; + padding: 0; + padding-bottom: 1rem; + margin: 0; +} + +.modal-warning{ + text-align: center; + font-size: small; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.spec.ts new file mode 100644 index 00000000..1634af74 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModalDeletePublicationComponent } from './modal-delete-publication.component'; + +describe('ModalDeletePublicationComponent', () => { + let component: ModalDeletePublicationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ModalDeletePublicationComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ModalDeletePublicationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts new file mode 100644 index 00000000..66223be0 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts @@ -0,0 +1,47 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Publication } from 'src/app/interfaces/publication'; +import { ProjectService } from 'src/app/services/project.service'; + +@Component({ + selector: 'app-modal-delete-publication', + templateUrl: './modal-delete-publication.component.html', + styleUrls: ['./modal-delete-publication.component.scss'], +}) +export class ModalDeletePublicationComponent implements OnInit { + @Input() deleteModalRef: BsModalRef; + @Input() publication: Publication; + + @Output() onSuccessfulDelete = new EventEmitter(); + @Output() onCancelDelete = new EventEmitter(); + + errorResult: String; + errorStatusAlert = false; + + constructor(private projectService: ProjectService) {} + + ngOnInit(): void {} + + onDeleteComment(): void { + this.projectService + .deletePublication(this.publication.publicationID) + .subscribe({ + next: () => { + console.log('Deleted publication', this.publication.publicationID); + this.onSuccessfulDelete.emit(); + this.deleteModalRef.hide(); + }, + error: (error) => { + this.errorStatusAlert = true; + this.errorResult = + 'Error: unable to delete publication, please contact developers for assistance'; + console.log(error); + }, + }); + } + + closeModal() { + this.onCancelDelete.emit(); + this.deleteModalRef.hide(); + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html new file mode 100644 index 00000000..d091aaa7 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html @@ -0,0 +1,94 @@ + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss new file mode 100644 index 00000000..afbc3637 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss @@ -0,0 +1,58 @@ +.modal-confirm{ + text-align: center; + font-weight: 700; + font-size: large; + padding: 0; + padding-bottom: 1rem; + margin: 0; +} + +.modal-warning{ + text-align: center; + font-size: small; + padding: 0; + margin: 0; +} + +.label-row { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: var(--space-3); + align-items: center; + margin-bottom: 1rem; +} + +.publication-form { + display: grid; + grid-template-columns: max-content 1fr; + gap: 1rem var(--space-3); + align-items: center; +} + +.label-title { + margin: 0; + text-align: right; + white-space: nowrap; +} + +.form-control { + width: 100%; + min-width: 0; +} + +.form-header { + margin-bottom: 1.5rem; +} + +.form-title { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: #111827; +} + +.form-subtitle { + margin: 0.35rem 0 0; + font-size: 0.92rem; + color: #6b7280; +} \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.spec.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.spec.ts new file mode 100644 index 00000000..cb4b7440 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModalEditPublicationComponent } from './modal-edit-publication.component'; + +describe('ModalEditPublicationComponent', () => { + let component: ModalEditPublicationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ModalEditPublicationComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ModalEditPublicationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts new file mode 100644 index 00000000..45f6af1c --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts @@ -0,0 +1,94 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + Validators, +} from '@angular/forms'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { User } from 'src/app/interfaces/user'; +import { ProjectService } from 'src/app/services/project.service'; + +@Component({ + selector: 'app-modal-edit-publication', + templateUrl: './modal-edit-publication.component.html', + styleUrls: ['./modal-edit-publication.component.scss'], +}) +export class ModalEditPublicationComponent implements OnInit { + @Input() editModalRef: BsModalRef; + @Input() currentUser: User; + @Input() projectID: number; + + @Output() onSuccessfulEdit = new EventEmitter(); + @Output() onCancelEdit = new EventEmitter(); + + errorResult: String; + errorStatusAlert = false; + + // Form + publicationForm: FormGroup; + title: FormControl; + url: FormControl; + doi: FormControl; + sourceAuthor: FormControl; + year: FormControl; + isLoading: boolean = false; + + constructor( + private projectService: ProjectService, + private formBuilder: FormBuilder, + ) {} + + ngOnInit(): void { + // Setup Form + this.title = new FormControl('', [Validators.required]); + this.url = new FormControl(''); + this.doi = new FormControl(''); + this.sourceAuthor = new FormControl(''); + this.year = new FormControl(''); + + // Initialize FormGroup using FormBuilder + this.publicationForm = this.formBuilder.group({ + title: this.title, + url: this.url, + doi: this.doi, + sourceAuthor: this.sourceAuthor, + year: this.year, + }); + } + + onAddPublication(): void { + let pub = this.publicationForm.value; + this.isLoading = true; + + const formData = new FormData(); + formData.append('projectID', String(this.projectID)); + formData.append('title', pub.title); + + if (pub.url) formData.append('url', pub.url); + if (pub.doi) formData.append('doi', pub.doi); + if (pub.sourceAuthor) formData.append('sourceAuthor', pub.sourceAuthor); + if (pub.year !== null && pub.year !== undefined && pub.year !== '') { + formData.append('year', String(pub.year)); + } + + this.projectService.addPublication(formData).subscribe({ + next: () => { + this.onSuccessfulEdit.emit(); + this.editModalRef.hide(); + }, + error: (error) => { + this.errorStatusAlert = true; + this.errorResult = + 'Error: unable to add publication, please contact developers for assistance'; + console.log(error); + this.isLoading = false; + }, + }); + } + + closeModal() { + this.onCancelEdit.emit(); + this.editModalRef.hide(); + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html index e1053d71..c2d6f682 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html @@ -1,56 +1,48 @@
- Publications + Publications
-
-
-
    -
  • -
    - Neuromorphic Computing and Engineering,2020 -
    - -
    -
    -
  • -
  • -
    - Neuromorphic Computing and Engineering,2020 -
    - -
    -
    -
  • -
-
- +
+
+ No publications yet. +
- diff --git a/src/Analysim.Web/ClientApp/src/app/projects/projects.module.ts b/src/Analysim.Web/ClientApp/src/app/projects/projects.module.ts index 5cb2a282..83964b9b 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/projects.module.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/projects.module.ts @@ -60,6 +60,9 @@ import { ProjectCommentItemComponent } from './project-comments/project-comment- import { ProjectCommentBoxComponent } from './project-comments/project-comment-box/project-comment-box.component'; import { ModalReportCommentComponent } from './project-comments/modal-report-comment/modal-report-comment.component'; import { ModalDeleteCommentComponent } from './project-comments/modal-delete-comment/modal-delete-comment.component'; +import { PublicationItemComponent } from './project-overview/project-overview-view/project-publication/publication-item/publication-item.component'; +import { ModalEditPublicationComponent } from './project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component'; +import { ModalDeletePublicationComponent } from './project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component'; @NgModule({ declarations: [ @@ -112,6 +115,9 @@ import { ModalDeleteCommentComponent } from './project-comments/modal-delete-com ProjectCommentBoxComponent, ModalReportCommentComponent, ModalDeleteCommentComponent, + PublicationItemComponent, + ModalEditPublicationComponent, + ModalDeletePublicationComponent, ], imports: [ CommonModule, diff --git a/src/Analysim.Web/ClientApp/src/app/services/project.service.ts b/src/Analysim.Web/ClientApp/src/app/services/project.service.ts index 510c7079..258d4ff1 100644 --- a/src/Analysim.Web/ClientApp/src/app/services/project.service.ts +++ b/src/Analysim.Web/ClientApp/src/app/services/project.service.ts @@ -16,6 +16,7 @@ import { Notebook, NotebookFile, NotebookURL } from '../interfaces/notebook'; import { getItem } from 'localforage'; import { ProjectComment } from '../interfaces/project-comment'; import { FlaggedCommentGroup } from '../interfaces/project-comment-flag'; +import { Publication } from '../interfaces/publication'; @Injectable({ providedIn: 'root' @@ -43,6 +44,7 @@ export class ProjectService { private urlGetNotebookVersions: string = this.baseUrl + "getnotebookversions/" private urlGetProjectComments: string = this.baseUrl + "getprojectcomments/"; private urlGetFlaggedProjectComments: string = this.baseUrl + "GetAllFlaggedComments"; + private urlGetPublications: string = this.baseUrl + "GetPublications/"; // Post private urlCreateProject: string = this.baseUrl + "createproject" @@ -58,6 +60,7 @@ export class ProjectService { private urlLikeComment: string = this.baseUrl + "likecomment/"; private urlReportComment: string = this.baseUrl + "reportcomment/"; private urlDeleteCommentandReports: string = this.baseUrl + "deletecommentandreports/"; + private urlAddPublication: string = this.baseUrl + "addPublication"; // Put private urlUpdateProject: string = this.baseUrl + "updateproject/" @@ -76,6 +79,7 @@ export class ProjectService { private urlUnlikeComment: string = this.baseUrl + "likecomment/"; private urlRemoveCommentReport: string = this.baseUrl + "removecommentreport/"; private urlRemoveAllCommentReports: string = this.baseUrl + "removeallcommentreports/"; + private urlDeletePublication: string = this.baseUrl + "deletepublication/"; // Extra private urlGetUserList: string = this.baseUrl + "getuserlist/" @@ -1012,4 +1016,54 @@ export class ProjectService { ); } + getPublications(projectID: number): Observable { + return this.http.get(this.urlGetPublications + projectID) + .pipe( + map(body => { + console.log(body.message); + return body.result; + }), + catchError(error => { + console.log(error); + return throwError(() => error); + }) + ); + } + + addPublication(formData: FormData): Observable { + const headers = new HttpHeaders().set( + 'Authorization', + `Bearer ${localStorage.getItem('jwt')}` + ); + + return this.http.post(this.urlAddPublication, formData, {headers}).pipe( + map(body => { + console.log(body.message); + return body; + }), + catchError(error => { + console.log(error); + return throwError(error); + }) + ); + } + + deletePublication(publicationId: number): Observable { + const headers = new HttpHeaders().set( + 'Authorization', + `Bearer ${localStorage.getItem('jwt')}` + ); + + return this.http.delete(this.urlDeletePublication + publicationId, {headers}).pipe( + map(body => { + console.log(body.message); + return body; + }), + catchError(error => { + console.log(error); + return throwError(error); + }) + ); + } + } diff --git a/src/Analysim.Web/Controllers/ProjectController.cs b/src/Analysim.Web/Controllers/ProjectController.cs index a9304ed5..a2798aeb 100644 --- a/src/Analysim.Web/Controllers/ProjectController.cs +++ b/src/Analysim.Web/Controllers/ProjectController.cs @@ -465,10 +465,100 @@ public async Task GetAllFlaggedComments() }); } + /* + * Type : GET + * URL : /api/projects/getPublications/projectId + * Description: Gets all publications for a project + */ + [HttpGet("[action]/{projectId}")] + public async Task GetPublications([FromRoute] int projectId) + { + if (projectId <= 0) + return BadRequest("Invalid project id."); + + var publications = await _dbContext.Publications + .AsNoTracking() + .Where(pr => pr.ProjectID == projectId) + .OrderBy(p => p.CreatedAt) + .Select(p => new PublicationVM + { + PublicationID = p.PublicationID, + ProjectID = p.ProjectID, + Title = p.Title, + Url = p.Url, + Doi = p.Doi, + SourceAuthor = p.SourceAuthor, + Year = p.Year, + CreatedAt = p.CreatedAt, + }).ToListAsync(); + + return Ok(new + { + result = publications, + message = "Received Publications" + }); + } + #endregion #region POST REQUEST + /* + * Type : POST + * URL : /api/project/addPublication + * Param : CreatePublicationVM + * Description: Add a new publication to a project + */ + [Authorize] + [HttpPost("[action]")] + public async Task AddPublication([FromForm] CreatePublicationVM formdata) + { + // Validate VM + if (!ModelState.IsValid) return BadRequest(ModelState); + + // Get User + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if(string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId)) + { + return Unauthorized(new {message = "Invalid user identifier."}); + } + + // Validate User + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.Id == userId); + if (user == null) return NotFound(new {message = "User Not Found."}); + + // Validate Project + var project = await _dbContext.Projects.FindAsync(formdata.ProjectID); + if (project == null) return NotFound(new { message = "Project Not Found" }); + + // Validate Title + if (string.IsNullOrWhiteSpace(formdata.Title)) + return BadRequest(new { message = "Title is required." }); + + // Create Publication + var newPublication = new Publication + { + ProjectID = formdata.ProjectID, + Project = project, + Title = formdata.Title, + Url = formdata.Url, + Doi = formdata.Doi, + SourceAuthor = formdata.SourceAuthor, + Year = formdata.Year, + CreatedAt = DateTime.UtcNow, + }; + + // Add Publication to DB + _dbContext.Publications.Add(newPublication); + await _dbContext.SaveChangesAsync(); + + // Return + return Ok(new + { + message = "Publication Added successfully." + }); + } + /* * Type : POST * URL : /api/project/postcomment @@ -2349,6 +2439,37 @@ public async Task RenameNotebook([FromForm] NotebookNameChangeVM #region DELETE REQUEST + /* + * Type : DELETE + * URL : /api/project/DeletePublication/{publicationId} + * Param : {publicationId} + * Description: deletes a publication + */ + [Authorize] + [HttpDelete("[action]/{publicationId}")] + public async Task DeletePublication([FromRoute] int publicationId) + { + // Get User + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId)) + { + return Unauthorized(new { message = "Invalid user identifier." }); + } + + // Find publication + var publication = await _dbContext.Publications + .FirstOrDefaultAsync(p => p.PublicationID == publicationId); + if (publication == null) return NotFound(new { message = "Publication not found." }); + + + // Remove publication + _dbContext.Publications.Remove(publication); + await _dbContext.SaveChangesAsync(); + + // Return + return Ok(new { message = "Publication deleted successfully."}); + } + /* * Type : DELETE * URL : /api/project/likecomment/{commentID} diff --git a/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs b/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs new file mode 100644 index 00000000..98658b7d --- /dev/null +++ b/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Analysim.Web.ViewModels.Project +{ + public class CreatePublicationVM + { + [Required(ErrorMessage = "Project ID is a required field.")] + public int ProjectID { get; set; } + + [Required(ErrorMessage = "Title is a required field.")] + public string Title { get; set; } = string.Empty; + public string? Url { get; set; } + public string? Doi { get; set; } + public string? SourceAuthor { get; set; } + public int? Year { get; set; } + } +} \ No newline at end of file diff --git a/src/Analysim.Web/ViewModels/Project/PublicationVM.cs b/src/Analysim.Web/ViewModels/Project/PublicationVM.cs new file mode 100644 index 00000000..a0f95aa1 --- /dev/null +++ b/src/Analysim.Web/ViewModels/Project/PublicationVM.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Analysim.Web.ViewModels.Project +{ + public class PublicationVM + { + public int PublicationID { get; set; } + public int ProjectID { get; set; } + public string Title { get; set; } = string.Empty; + public string? Url { get; set; } + public string? Doi { get; set; } + public string? SourceAuthor { get; set; } + public int? Year { get; set; } + public DateTime CreatedAt { get; set; } + } +} \ No newline at end of file From bfcb86cf3436cf336005f37a59d81feaf3d664fc Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 1 May 2026 14:02:28 -0400 Subject: [PATCH 2/2] feat: Add fields, updating, and sorting to Publications Added journal and notes to publications Publications require journal, author, and year Publications no longer require titles Fixed publication display order Implemented editing to publications Added local sorting by publication year --- src/Analysim.Core/Entities/Publication.cs | 14 +- .../ApplicationDbContextModelSnapshot.cs | 16 +- ...60501153334_UpdatePublications.Designer.cs | 996 ++++++++++++++++++ .../20260501153334_UpdatePublications.cs | 132 +++ .../src/app/interfaces/publication.ts | 2 + .../modal-delete-publication.component.html | 2 +- .../modal-delete-publication.component.scss | 2 +- .../modal-delete-publication.component.ts | 7 +- .../modal-edit-publication.component.html | 82 +- .../modal-edit-publication.component.scss | 16 +- .../modal-edit-publication.component.ts | 124 ++- .../project-publication.component.html | 44 +- .../project-publication.component.scss | 68 ++ .../project-publication.component.ts | 29 + .../publication-item.component.html | 33 +- .../publication-item.component.scss | 4 + .../publication-item.component.ts | 21 +- .../src/app/services/project.service.ts | 19 + .../Controllers/ProjectController.cs | 83 +- .../ViewModels/Project/CreatePublicationVM.cs | 14 +- .../ViewModels/Project/PublicationVM.cs | 6 +- 21 files changed, 1616 insertions(+), 98 deletions(-) create mode 100644 src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.Designer.cs create mode 100644 src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.cs diff --git a/src/Analysim.Core/Entities/Publication.cs b/src/Analysim.Core/Entities/Publication.cs index bdca5644..4887039c 100644 --- a/src/Analysim.Core/Entities/Publication.cs +++ b/src/Analysim.Core/Entities/Publication.cs @@ -17,12 +17,20 @@ public class Publication public Project Project{ get; set; } = null!; // publication content - [Required(ErrorMessage = "Publication Title is a required field.")] - public string Title { get; set; } = string.Empty; + public string? Title { get; set; } + + [Required(ErrorMessage = "Publication Journal / Conference is a required field.")] + public string Journal { get; set; } = string.Empty; public string? Url { get; set; } public string? Doi { get; set; } - public string? SourceAuthor { get; set; } + + [Required(ErrorMessage = "Publication Author is a required field.")] + public string SourceAuthor { get; set; } = string.Empty; + + [Required(ErrorMessage = "Publication Year is a required field.")] + [Range(1, 2100, ErrorMessage = "Enter a valid publication year.")] public int? Year { get; set; } + public string? Notes { get; set; } // Timestamps public DateTime CreatedAt { get; set; } diff --git a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index fc110a57..d1189288 100644 --- a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -388,20 +388,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Doi") .HasColumnType("text"); + b.Property("Journal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + b.Property("ProjectID") .HasColumnType("integer"); b.Property("SourceAuthor") + .IsRequired() .HasColumnType("text"); b.Property("Title") - .IsRequired() .HasColumnType("text"); b.Property("Url") .HasColumnType("text"); b.Property("Year") + .IsRequired() .HasColumnType("integer"); b.HasKey("PublicationID"); @@ -556,21 +564,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 1, - ConcurrencyStamp = "e2465fb1-9027-4762-9172-0ddfe8ba46ff", + ConcurrencyStamp = "f1d80f82-74ad-4f0e-b39a-228c651b424a", Name = "Admin", NormalizedName = "ADMIN" }, new { Id = 2, - ConcurrencyStamp = "d9b0d06b-709c-4ee9-82c5-e733494e0a9a", + ConcurrencyStamp = "cfb8a477-eb7b-4e91-865d-122d17565ce0", Name = "Customer", NormalizedName = "CUSTOMER" }, new { Id = 3, - ConcurrencyStamp = "fdf254aa-d0e4-4a22-ad0c-b3fb12cb69c7", + ConcurrencyStamp = "ecaa6f71-1595-44f7-9337-57b461da675a", Name = "Moderator", NormalizedName = "MODERATOR" }); diff --git a/src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.Designer.cs b/src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.Designer.cs new file mode 100644 index 00000000..7257f9d0 --- /dev/null +++ b/src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.Designer.cs @@ -0,0 +1,996 @@ +// +using System; +using Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260501153334_UpdatePublications")] + partial class UpdatePublications + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Analysim.Core.Entities.BlobFileContent", b => + { + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("BlobFileID"); + + b.ToTable("BlobFileContent"); + }); + + modelBuilder.Entity("Analysim.Core.Entities.NotebookContent", b => + { + b.Property("NotebookID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("Version") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("Content") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Size") + .HasColumnType("integer"); + + b.HasKey("NotebookID", "Version"); + + b.ToTable("NotebookContent"); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.Property("BlobFileID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BlobFileID")); + + b.Property("Container") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Directory") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("BlobFileID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID"); + + b.ToTable("BlobFiles"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.Property("NotebookID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NotebookID")); + + b.Property("Container") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Directory") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("Route") + .IsRequired() + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("Uri") + .IsRequired() + .HasColumnType("text"); + + b.Property("type") + .HasColumnType("text"); + + b.HasKey("NotebookID"); + + b.HasIndex("Directory"); + + b.HasIndex("ProjectID"); + + b.ToTable("Notebook"); + }); + + modelBuilder.Entity("Core.Entities.ObservableNotebookDataset", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ID")); + + b.Property("BlobFileID") + .HasColumnType("integer"); + + b.Property("NotebookID") + .HasColumnType("integer"); + + b.Property("datasetName") + .HasColumnType("text"); + + b.Property("datasetURL") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("NotebookID"); + + b.ToTable("ObservableNotebookDataset"); + }); + + modelBuilder.Entity("Core.Entities.Project", b => + { + b.Property("ProjectID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ProjectID")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ForkedFromProjectID") + .HasColumnType("integer"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Route") + .IsRequired() + .HasColumnType("text"); + + b.Property("Visibility") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ProjectID"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.Property("CommentID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CommentID")); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsPendingReview") + .HasColumnType("boolean"); + + b.Property("ParentCommentID") + .HasColumnType("integer"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("CommentID"); + + b.HasIndex("ParentCommentID"); + + b.HasIndex("ProjectID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectComments"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentFlag", b => + { + b.Property("FlagID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("FlagID")); + + b.Property("CommentContentSnapshot") + .HasColumnType("text"); + + b.Property("CommentID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.HasKey("FlagID"); + + b.HasIndex("UserID"); + + b.HasIndex("CommentID", "UserID") + .IsUnique(); + + b.ToTable("ProjectCommentFlags"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentLike", b => + { + b.Property("CommentID") + .HasColumnType("integer"); + + b.Property("UserID") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CommentID", "UserID"); + + b.HasIndex("UserID"); + + b.ToTable("ProjectCommentLikes"); + }); + + modelBuilder.Entity("Core.Entities.ProjectTag", b => + { + b.Property("ProjectID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("TagID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.HasKey("ProjectID", "TagID"); + + b.HasIndex("TagID"); + + b.ToTable("ProjectTags"); + }); + + modelBuilder.Entity("Core.Entities.ProjectUser", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("ProjectID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.Property("IsFollowing") + .HasColumnType("boolean"); + + b.Property("UserRole") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserID", "ProjectID"); + + b.HasIndex("ProjectID"); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.Property("PublicationID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PublicationID")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Doi") + .HasColumnType("text"); + + b.Property("Journal") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("ProjectID") + .HasColumnType("integer"); + + b.Property("SourceAuthor") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Year") + .IsRequired() + .HasColumnType("integer"); + + b.HasKey("PublicationID"); + + b.HasIndex("ProjectID"); + + b.ToTable("Publications"); + }); + + modelBuilder.Entity("Core.Entities.Tag", b => + { + b.Property("TagID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TagID")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("TagID"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastOnline") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("RegistrationSurvey") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Core.Entities.UserUser", b => + { + b.Property("UserID") + .HasColumnType("integer") + .HasColumnOrder(1); + + b.Property("FollowerID") + .HasColumnType("integer") + .HasColumnOrder(2); + + b.HasKey("UserID", "FollowerID"); + + b.HasIndex("FollowerID"); + + b.ToTable("UserUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = 1, + ConcurrencyStamp = "f1d80f82-74ad-4f0e-b39a-228c651b424a", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = 2, + ConcurrencyStamp = "cfb8a477-eb7b-4e91-865d-122d17565ce0", + Name = "Customer", + NormalizedName = "CUSTOMER" + }, + new + { + Id = 3, + ConcurrencyStamp = "ecaa6f71-1595-44f7-9337-57b461da675a", + Name = "Moderator", + NormalizedName = "MODERATOR" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Analysim.Core.Entities.BlobFileContent", b => + { + b.HasOne("Core.Entities.BlobFile", "BlobFile") + .WithMany("BlobFileContents") + .HasForeignKey("BlobFileID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlobFile"); + }); + + modelBuilder.Entity("Analysim.Core.Entities.NotebookContent", b => + { + b.HasOne("Core.Entities.Notebook", "Notebook") + .WithMany("NotebookContents") + .HasForeignKey("NotebookID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notebook"); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("BlobFiles") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Entities.User", "User") + .WithMany("BlobFiles") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Notebooks") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Core.Entities.ObservableNotebookDataset", b => + { + b.HasOne("Core.Entities.Notebook", "notebook") + .WithMany("observableNotebookDatasets") + .HasForeignKey("NotebookID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("notebook"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.HasOne("Core.Entities.ProjectComment", "ParentComment") + .WithMany("Replies") + .HasForeignKey("ParentCommentID") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectComments") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectComments") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ParentComment"); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentFlag", b => + { + b.HasOne("Core.Entities.ProjectComment", "ProjectComment") + .WithMany("CommentFlags") + .HasForeignKey("CommentID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("CommentFlags") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectComment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectCommentLike", b => + { + b.HasOne("Core.Entities.ProjectComment", "ProjectComment") + .WithMany("CommentLikes") + .HasForeignKey("CommentID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("CommentLikes") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectComment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.ProjectTag", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectTags") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Tag", "Tag") + .WithMany("ProjectTags") + .HasForeignKey("TagID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Core.Entities.ProjectUser", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("ProjectUsers") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("ProjectUsers") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Core.Entities.Publication", b => + { + b.HasOne("Core.Entities.Project", "Project") + .WithMany("Publications") + .HasForeignKey("ProjectID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("Core.Entities.UserUser", b => + { + b.HasOne("Core.Entities.User", "Follower") + .WithMany("Following") + .HasForeignKey("FollowerID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", "User") + .WithMany("Followers") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Follower"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.BlobFile", b => + { + b.Navigation("BlobFileContents"); + }); + + modelBuilder.Entity("Core.Entities.Notebook", b => + { + b.Navigation("NotebookContents"); + + b.Navigation("observableNotebookDatasets"); + }); + + modelBuilder.Entity("Core.Entities.Project", b => + { + b.Navigation("BlobFiles"); + + b.Navigation("Notebooks"); + + b.Navigation("ProjectComments"); + + b.Navigation("ProjectTags"); + + b.Navigation("ProjectUsers"); + + b.Navigation("Publications"); + }); + + modelBuilder.Entity("Core.Entities.ProjectComment", b => + { + b.Navigation("CommentFlags"); + + b.Navigation("CommentLikes"); + + b.Navigation("Replies"); + }); + + modelBuilder.Entity("Core.Entities.Tag", b => + { + b.Navigation("ProjectTags"); + }); + + modelBuilder.Entity("Core.Entities.User", b => + { + b.Navigation("BlobFiles"); + + b.Navigation("CommentFlags"); + + b.Navigation("CommentLikes"); + + b.Navigation("Followers"); + + b.Navigation("Following"); + + b.Navigation("ProjectComments"); + + b.Navigation("ProjectUsers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.cs b/src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.cs new file mode 100644 index 00000000..91e64385 --- /dev/null +++ b/src/Analysim.Infrastructure/Migrations/20260501153334_UpdatePublications.cs @@ -0,0 +1,132 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + public partial class UpdatePublications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Year", + table: "Publications", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Title", + table: "Publications", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "SourceAuthor", + table: "Publications", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Journal", + table: "Publications", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Notes", + table: "Publications", + type: "text", + nullable: true); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 1, + column: "ConcurrencyStamp", + value: "f1d80f82-74ad-4f0e-b39a-228c651b424a"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 2, + column: "ConcurrencyStamp", + value: "cfb8a477-eb7b-4e91-865d-122d17565ce0"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 3, + column: "ConcurrencyStamp", + value: "ecaa6f71-1595-44f7-9337-57b461da675a"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Journal", + table: "Publications"); + + migrationBuilder.DropColumn( + name: "Notes", + table: "Publications"); + + migrationBuilder.AlterColumn( + name: "Year", + table: "Publications", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "Title", + table: "Publications", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SourceAuthor", + table: "Publications", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 1, + column: "ConcurrencyStamp", + value: "e2465fb1-9027-4762-9172-0ddfe8ba46ff"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 2, + column: "ConcurrencyStamp", + value: "d9b0d06b-709c-4ee9-82c5-e733494e0a9a"); + + migrationBuilder.UpdateData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: 3, + column: "ConcurrencyStamp", + value: "fdf254aa-d0e4-4a22-ad0c-b3fb12cb69c7"); + } + } +} diff --git a/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts b/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts index f33c3a8a..066a05c8 100644 --- a/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts +++ b/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts @@ -1,9 +1,11 @@ export interface Publication { publicationID: number; title: string; + journal: string; url: string; doi: string; sourceAuthor: string; year: number; + notes: string; createdAt: string; } \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html index 82cbb4fd..0e9da75a 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.html @@ -9,7 +9,7 @@

diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss index e4caf7ff..556f3410 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.scss @@ -9,7 +9,7 @@ .modal-warning{ text-align: center; - font-size: small; + font-size: medium; padding: 0; margin: 0; } \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts index 66223be0..bf37017a 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-delete-publication/modal-delete-publication.component.ts @@ -18,9 +18,14 @@ export class ModalDeletePublicationComponent implements OnInit { errorResult: String; errorStatusAlert = false; + deleteTitle = "This Publication"; + constructor(private projectService: ProjectService) {} - ngOnInit(): void {} + ngOnInit(): void { + if(this.publication.title != null) this.deleteTitle = this.publication.title; + else this.deleteTitle = this.publication.journal; + } onDeleteComment(): void { this.projectService diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html index d091aaa7..c9a7d013 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.html @@ -1,24 +1,44 @@ diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss index afbc3637..dca488f4 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.scss @@ -26,7 +26,7 @@ display: grid; grid-template-columns: max-content 1fr; gap: 1rem var(--space-3); - align-items: center; + align-items: start; } .label-title { @@ -38,21 +38,13 @@ .form-control { width: 100%; min-width: 0; + color: black !important; } .form-header { margin-bottom: 1.5rem; } -.form-title { - margin: 0; - font-size: 1.125rem; - font-weight: 600; - color: #111827; -} - -.form-subtitle { - margin: 0.35rem 0 0; - font-size: 0.92rem; - color: #6b7280; +.error-center{ + text-align: center; } \ No newline at end of file diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts index 45f6af1c..d1049180 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/modal-edit-publication/modal-edit-publication.component.ts @@ -6,7 +6,7 @@ import { Validators, } from '@angular/forms'; import { BsModalRef } from 'ngx-bootstrap/modal'; -import { User } from 'src/app/interfaces/user'; +import { Publication } from 'src/app/interfaces/publication'; import { ProjectService } from 'src/app/services/project.service'; @Component({ @@ -15,16 +15,19 @@ import { ProjectService } from 'src/app/services/project.service'; styleUrls: ['./modal-edit-publication.component.scss'], }) export class ModalEditPublicationComponent implements OnInit { + @Input() editingPublication: Publication | null; + @Input() editModalRef: BsModalRef; - @Input() currentUser: User; @Input() projectID: number; @Output() onSuccessfulEdit = new EventEmitter(); @Output() onCancelEdit = new EventEmitter(); - errorResult: String; + errorResult: string; errorStatusAlert = false; + modalTitle = ''; + // Form publicationForm: FormGroup; title: FormControl; @@ -32,6 +35,8 @@ export class ModalEditPublicationComponent implements OnInit { doi: FormControl; sourceAuthor: FormControl; year: FormControl; + journal: FormControl; + notes: FormControl; isLoading: boolean = false; constructor( @@ -40,36 +45,42 @@ export class ModalEditPublicationComponent implements OnInit { ) {} ngOnInit(): void { + if (this.editingPublication) this.modalTitle = 'Edit Publication'; + else this.modalTitle = 'Add Publication'; + // Setup Form - this.title = new FormControl('', [Validators.required]); - this.url = new FormControl(''); - this.doi = new FormControl(''); - this.sourceAuthor = new FormControl(''); - this.year = new FormControl(''); + const ep = this.editingPublication; + this.title = new FormControl(ep ? (ep.title ? ep.title : '') : '', [ + Validators.required, + ]); + this.url = new FormControl(ep ? (ep.url ? ep.url : '') : ''); + this.doi = new FormControl(ep ? (ep.doi ? ep.doi : '') : ''); + this.sourceAuthor = new FormControl(ep ? (ep.sourceAuthor ? ep.sourceAuthor : '') : ''); + this.year = new FormControl(ep ? (ep.year ? ep.year : '') : ''); + this.journal = new FormControl(ep ? (ep.journal ? ep.journal : '') : ''); + this.notes = new FormControl(ep ? (ep.notes ? ep.notes : '') : ''); // Initialize FormGroup using FormBuilder this.publicationForm = this.formBuilder.group({ title: this.title, + journal: this.journal, url: this.url, doi: this.doi, sourceAuthor: this.sourceAuthor, year: this.year, + notes: this.notes, }); } onAddPublication(): void { - let pub = this.publicationForm.value; + this.errorStatusAlert = false; + this.errorResult = null; this.isLoading = true; - const formData = new FormData(); - formData.append('projectID', String(this.projectID)); - formData.append('title', pub.title); - - if (pub.url) formData.append('url', pub.url); - if (pub.doi) formData.append('doi', pub.doi); - if (pub.sourceAuthor) formData.append('sourceAuthor', pub.sourceAuthor); - if (pub.year !== null && pub.year !== undefined && pub.year !== '') { - formData.append('year', String(pub.year)); + const formData = this.buildForm(); + if(formData == null) { + this.isLoading = false; + return; } this.projectService.addPublication(formData).subscribe({ @@ -78,17 +89,86 @@ export class ModalEditPublicationComponent implements OnInit { this.editModalRef.hide(); }, error: (error) => { - this.errorStatusAlert = true; - this.errorResult = - 'Error: unable to add publication, please contact developers for assistance'; console.log(error); - this.isLoading = false; + this.handleError( + 'Error: unable to add publication, please contact developers for assistance', + ); }, }); } + onEditPublication(){ + this.errorStatusAlert = false; + this.errorResult = null; + this.isLoading = true; + + const formData = this.buildForm(); + if(formData == null) { + this.isLoading = false; + return; + } + + if (!this.editingPublication) { + this.handleError('Error: No publication selected for editing.'); + return; + } + + this.projectService.updatePublication(formData, this.editingPublication.publicationID).subscribe({ + next: () => { + this.onSuccessfulEdit.emit(); + this.editModalRef.hide(); + }, + error: (error) => { + console.log(error); + this.handleError( + 'Error: unable to update publication, please contact developers for assistance', + ); + }, + }); + } + + buildForm(): FormData | null { + let pub = this.publicationForm.value; + + const formData = new FormData(); + formData.append('projectID', String(this.projectID)); + + // validate required fields + if (pub.sourceAuthor) { + formData.append('sourceAuthor', pub.sourceAuthor); + } else { + this.handleError('Error: No source author provided'); + return null; + } + if (pub.year !== null && pub.year !== undefined && pub.year !== '') { + formData.append('year', String(pub.year)); + } else { + this.handleError('Error: No valid year provided'); + return null; + } + if (pub.journal) { + formData.append('journal', pub.journal); + } else { + this.handleError('Error: No publication journal provided'); + return null; + } + + if (pub.title) formData.append('title', pub.title); + if (pub.url) formData.append('url', pub.url); + if (pub.doi) formData.append('doi', pub.doi); + if (pub.notes) formData.append('notes', pub.notes); + + return formData; + } + closeModal() { this.onCancelEdit.emit(); this.editModalRef.hide(); } + + handleError(text: string) { + this.errorStatusAlert = true; + this.errorResult = text; + this.isLoading = false; + } } diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html index c2d6f682..325ae1aa 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/project-publication.component.html @@ -1,7 +1,45 @@
Publications -
+
+
+ + +
+ + +
+ + + +
+
+
+
+
-
+
URL: {{ publication.url }} @@ -30,6 +37,7 @@ >
+

{{ publication.notes }}

@@ -38,7 +46,18 @@ + + + + + diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.scss b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.scss index fbb739a2..8c6808a5 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.scss +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.scss @@ -13,6 +13,10 @@ margin: 0% !important; } +.url-row{ + margin-bottom: var(--space-3) !important; +} + .btn-width { width: 2.5rem !important; } diff --git a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.ts b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.ts index 2d9ba061..be024bb0 100644 --- a/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.ts +++ b/src/Analysim.Web/ClientApp/src/app/projects/project-overview/project-overview-view/project-publication/publication-item/publication-item.component.ts @@ -20,13 +20,17 @@ export class PublicationItemComponent implements OnInit { @Input() publication!: Publication; @Input() isMember: boolean; @Input() currentUser: User; + @Input() projectId: number; @Output() onReload = new EventEmitter(); // Modals @ViewChild('deleteModal') deleteModal: TemplateRef; deleteModalRef: BsModalRef; - isDeleting = false; + @ViewChild('editModal') editModal: TemplateRef; + editModalRef: BsModalRef; + + isHandlingAction = false; constructor( private modalService: BsModalService @@ -36,15 +40,24 @@ export class PublicationItemComponent implements OnInit { onDeletePublication(): void { this.toggleModalDelete(); - this.isDeleting = true; + this.isHandlingAction = true; } - onHandleSuccessfulDelete(): void { - this.isDeleting = false; + onHandleSuccessfulAction(): void { + this.isHandlingAction = false; this.onReload.emit(); } toggleModalDelete() { this.deleteModalRef = this.modalService.show(this.deleteModal); } + + onEditPublication(): void{ + this.toggleModalEdit(); + this.isHandlingAction = true; + } + + toggleModalEdit(): void{ + this.editModalRef = this.modalService.show(this.editModal); + } } diff --git a/src/Analysim.Web/ClientApp/src/app/services/project.service.ts b/src/Analysim.Web/ClientApp/src/app/services/project.service.ts index 258d4ff1..ab1cec59 100644 --- a/src/Analysim.Web/ClientApp/src/app/services/project.service.ts +++ b/src/Analysim.Web/ClientApp/src/app/services/project.service.ts @@ -70,6 +70,7 @@ export class ProjectService { private urlDeleteDatasetFromNotebook: string = this.baseUrl + "deleteDatasetFromNotebook/" private urlUpdateComment: string = this.baseUrl + "updatecomment/"; private urlDeleteComment: string = this.baseUrl + "deletecomment/"; + private urlUpdatePublication: string = this.baseUrl + "updatePublication/"; // Delete private urlDeleteProject: string = this.baseUrl + "deleteproject/" @@ -1066,4 +1067,22 @@ export class ProjectService { ); } + updatePublication(formData: FormData, publicationId: number): Observable { + const headers = new HttpHeaders().set( + 'Authorization', + `Bearer ${localStorage.getItem('jwt')}` + ); + + return this.http.put(this.urlUpdatePublication + publicationId, formData, {headers}).pipe( + map(body => { + console.log(body.message); + return body; + }), + catchError(error => { + console.log(error); + return throwError(error); + }) + ); + } + } diff --git a/src/Analysim.Web/Controllers/ProjectController.cs b/src/Analysim.Web/Controllers/ProjectController.cs index a2798aeb..fe79e857 100644 --- a/src/Analysim.Web/Controllers/ProjectController.cs +++ b/src/Analysim.Web/Controllers/ProjectController.cs @@ -485,10 +485,12 @@ public async Task GetPublications([FromRoute] int projectId) PublicationID = p.PublicationID, ProjectID = p.ProjectID, Title = p.Title, + Journal = p.Journal, Url = p.Url, Doi = p.Doi, SourceAuthor = p.SourceAuthor, Year = p.Year, + Notes = p.Notes, CreatedAt = p.CreatedAt, }).ToListAsync(); @@ -531,9 +533,13 @@ public async Task AddPublication([FromForm] CreatePublicationVM f var project = await _dbContext.Projects.FindAsync(formdata.ProjectID); if (project == null) return NotFound(new { message = "Project Not Found" }); - // Validate Title - if (string.IsNullOrWhiteSpace(formdata.Title)) - return BadRequest(new { message = "Title is required." }); + // Validate Fields + if (string.IsNullOrWhiteSpace(formdata.Journal)) + return BadRequest(new { message = "Publication Journal is required." }); + if (string.IsNullOrWhiteSpace(formdata.SourceAuthor)) + return BadRequest(new { message = "Source Author is required." }); + if (!formdata.Year.HasValue || formdata.Year <= 0) + return BadRequest(new { message = "Valid Publication Year is required." }); // Create Publication var newPublication = new Publication @@ -541,10 +547,12 @@ public async Task AddPublication([FromForm] CreatePublicationVM f ProjectID = formdata.ProjectID, Project = project, Title = formdata.Title, + Journal = formdata.Journal, Url = formdata.Url, Doi = formdata.Doi, SourceAuthor = formdata.SourceAuthor, Year = formdata.Year, + Notes = formdata.Notes, CreatedAt = DateTime.UtcNow, }; @@ -2109,6 +2117,75 @@ public async Task AddDatasetToNotebook([FromForm] int notebookID, #region PUT REQUEST + /* + * Type : PUT + * URL : /api/project/updatePublication/{publicationID} + * Param : {publicationID}, CreatePublicationVM + * Description: Update a publication + */ + [Authorize] + [HttpPut("[action]/{publicationID}")] + public async Task UpdatePublication([FromRoute] int publicationID, [FromForm] CreatePublicationVM formdata) + { + // Validate VM + if (!ModelState.IsValid) return BadRequest(ModelState); + + // Get User + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userIdClaim) || !int.TryParse(userIdClaim, out var userId)) + { + return Unauthorized(new { message = "Invalid user identifier." }); + } + + // Validate User + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.Id == userId); + if (user == null) return NotFound(new { message = "User Not Found." }); + + // Validate Publication + var publication = await _dbContext.Publications + .SingleOrDefaultAsync(p => p.PublicationID == publicationID); + + if (publication == null) + return NotFound(new { message = "Publication Not Found" }); + + // Validate Project + var project = await _dbContext.Projects.FindAsync(formdata.ProjectID); + if (project == null) + return NotFound(new { message = "Project Not Found" }); + + if (publication.ProjectID != formdata.ProjectID) + return BadRequest(new { message = "Publication does not belong to this project." }); + + // Validate Fields + if (string.IsNullOrWhiteSpace(formdata.Journal)) + return BadRequest(new { message = "Publication Journal is required." }); + + if (string.IsNullOrWhiteSpace(formdata.SourceAuthor)) + return BadRequest(new { message = "Source Author is required." }); + + if (!formdata.Year.HasValue || formdata.Year <= 0) + return BadRequest(new { message = "Valid Publication Year is required." }); + + // Update Publication + publication.Title = formdata.Title; + publication.Journal = formdata.Journal; + publication.Url = formdata.Url; + publication.Doi = formdata.Doi; + publication.SourceAuthor = formdata.SourceAuthor; + publication.Year = formdata.Year; + publication.Notes = formdata.Notes; + + // Update Publication in DB + await _dbContext.SaveChangesAsync(); + + // Return + return Ok(new + { + message = "Publication updated successfully." + }); + } + + /* * Type : PUT * URL : /api/project/updatecomment/{commentID} diff --git a/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs b/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs index 98658b7d..03cc30d2 100644 --- a/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs +++ b/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs @@ -7,11 +7,19 @@ public class CreatePublicationVM [Required(ErrorMessage = "Project ID is a required field.")] public int ProjectID { get; set; } - [Required(ErrorMessage = "Title is a required field.")] - public string Title { get; set; } = string.Empty; + public string? Title { get; set; } + + [Required(ErrorMessage = "Publication Journal / Conference is a required field.")] + public string Journal { get; set; } = string.Empty; public string? Url { get; set; } public string? Doi { get; set; } - public string? SourceAuthor { get; set; } + + [Required(ErrorMessage = "Publication Author is a required field.")] + public string SourceAuthor { get; set; } = string.Empty; + + [Required(ErrorMessage = "Publication Year is a required field.")] + [Range(1, 2100, ErrorMessage = "Enter a valid publication year.")] public int? Year { get; set; } + public string? Notes { get; set; } } } \ No newline at end of file diff --git a/src/Analysim.Web/ViewModels/Project/PublicationVM.cs b/src/Analysim.Web/ViewModels/Project/PublicationVM.cs index a0f95aa1..3b43685d 100644 --- a/src/Analysim.Web/ViewModels/Project/PublicationVM.cs +++ b/src/Analysim.Web/ViewModels/Project/PublicationVM.cs @@ -7,11 +7,13 @@ public class PublicationVM { public int PublicationID { get; set; } public int ProjectID { get; set; } - public string Title { get; set; } = string.Empty; + public string? Title { get; set; } + public string Journal { get; set; } = string.Empty; public string? Url { get; set; } public string? Doi { get; set; } - public string? SourceAuthor { get; set; } + public string SourceAuthor { get; set; } = string.Empty; public int? Year { get; set; } + public string? Notes { get; set; } public DateTime CreatedAt { get; set; } } } \ No newline at end of file