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..4887039c --- /dev/null +++ b/src/Analysim.Core/Entities/Publication.cs @@ -0,0 +1,38 @@ +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 + 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; } + + [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; } + } +} \ 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..d1189288 100644 --- a/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Analysim.Infrastructure/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -374,6 +374,51 @@ 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("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") @@ -519,21 +564,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 1, - ConcurrencyStamp = "45fc9c92-801d-433f-ab00-280c2334bc89", + ConcurrencyStamp = "f1d80f82-74ad-4f0e-b39a-228c651b424a", Name = "Admin", NormalizedName = "ADMIN" }, new { Id = 2, - ConcurrencyStamp = "922f6db8-d335-4aa4-9331-12097412cf5e", + ConcurrencyStamp = "cfb8a477-eb7b-4e91-865d-122d17565ce0", Name = "Customer", NormalizedName = "CUSTOMER" }, new { Id = 3, - ConcurrencyStamp = "cecf34c5-c9e0-4b99-9fb7-1441045209d8", + ConcurrencyStamp = "ecaa6f71-1595-44f7-9337-57b461da675a", Name = "Moderator", NormalizedName = "MODERATOR" }); @@ -805,6 +850,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 +954,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.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 new file mode 100644 index 00000000..066a05c8 --- /dev/null +++ b/src/Analysim.Web/ClientApp/src/app/interfaces/publication.ts @@ -0,0 +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 new file mode 100644 index 00000000..0e9da75a --- /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..556f3410 --- /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: 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.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..bf37017a --- /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,52 @@ +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; + + deleteTitle = "This Publication"; + + constructor(private projectService: ProjectService) {} + + ngOnInit(): void { + if(this.publication.title != null) this.deleteTitle = this.publication.title; + else this.deleteTitle = this.publication.journal; + } + + 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..c9a7d013 --- /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,112 @@ + 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..dca488f4 --- /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,50 @@ +.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: start; +} + +.label-title { + margin: 0; + text-align: right; + white-space: nowrap; +} + +.form-control { + width: 100%; + min-width: 0; + color: black !important; +} + +.form-header { + margin-bottom: 1.5rem; +} + +.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.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..d1049180 --- /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,174 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + Validators, +} from '@angular/forms'; +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-edit-publication', + templateUrl: './modal-edit-publication.component.html', + styleUrls: ['./modal-edit-publication.component.scss'], +}) +export class ModalEditPublicationComponent implements OnInit { + @Input() editingPublication: Publication | null; + + @Input() editModalRef: BsModalRef; + @Input() projectID: number; + + @Output() onSuccessfulEdit = new EventEmitter(); + @Output() onCancelEdit = new EventEmitter(); + + errorResult: string; + errorStatusAlert = false; + + modalTitle = ''; + + // Form + publicationForm: FormGroup; + title: FormControl; + url: FormControl; + doi: FormControl; + sourceAuthor: FormControl; + year: FormControl; + journal: FormControl; + notes: FormControl; + isLoading: boolean = false; + + constructor( + private projectService: ProjectService, + private formBuilder: FormBuilder, + ) {} + + ngOnInit(): void { + if (this.editingPublication) this.modalTitle = 'Edit Publication'; + else this.modalTitle = 'Add Publication'; + + // Setup Form + 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 { + this.errorStatusAlert = false; + this.errorResult = null; + this.isLoading = true; + + const formData = this.buildForm(); + if(formData == null) { + this.isLoading = false; + return; + } + + this.projectService.addPublication(formData).subscribe({ + next: () => { + this.onSuccessfulEdit.emit(); + this.editModalRef.hide(); + }, + error: (error) => { + console.log(error); + 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 e1053d71..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,56 +1,86 @@
- 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..ab1cec59 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/" @@ -67,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/" @@ -76,6 +80,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 +1017,72 @@ 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); + }) + ); + } + + 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 a9304ed5..fe79e857 100644 --- a/src/Analysim.Web/Controllers/ProjectController.cs +++ b/src/Analysim.Web/Controllers/ProjectController.cs @@ -465,10 +465,108 @@ 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, + Journal = p.Journal, + Url = p.Url, + Doi = p.Doi, + SourceAuthor = p.SourceAuthor, + Year = p.Year, + Notes = p.Notes, + 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 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 + { + 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, + }; + + // 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 @@ -2019,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} @@ -2349,6 +2516,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..03cc30d2 --- /dev/null +++ b/src/Analysim.Web/ViewModels/Project/CreatePublicationVM.cs @@ -0,0 +1,25 @@ +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; } + + 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; } + + [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 new file mode 100644 index 00000000..3b43685d --- /dev/null +++ b/src/Analysim.Web/ViewModels/Project/PublicationVM.cs @@ -0,0 +1,19 @@ +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; } + public string Journal { get; set; } = string.Empty; + public string? Url { get; set; } + public string? Doi { 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