diff --git a/PC2/Controllers/ProgramVideosController.cs b/PC2/Controllers/ProgramVideosController.cs index 9efd0451..fd46ba35 100644 --- a/PC2/Controllers/ProgramVideosController.cs +++ b/PC2/Controllers/ProgramVideosController.cs @@ -1,12 +1,154 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using PC2.Data; +using PC2.Models; namespace PC2.Controllers { + [Authorize(Roles = IdentityHelper.Admin)] public class ProgramVideosController : Controller { - public IActionResult Index() + private readonly ApplicationDbContext _context; + private readonly AzureBlobUploader _azureBlobUploader; + + public ProgramVideosController(ApplicationDbContext context, AzureBlobUploader azureBlobUploader) + { + _context = context; + _azureBlobUploader = azureBlobUploader; + } + + [AllowAnonymous] + public async Task Index() + { + List videos = await ProgramVideoDB.GetAllAsync(_context); + return View(videos); + } + + public async Task ManageVideos() + { + List videos = await ProgramVideoDB.GetAllAsync(_context); + return View(videos); + } + + [HttpGet] + public IActionResult CreateVideo() { return View(); } + + [HttpPost] + public async Task CreateVideo(ProgramVideo programVideo, IFormFile? pdfFile) + { + // Remove PDF-related fields from model validation since they are optional and set by the upload + ModelState.Remove(nameof(ProgramVideo.PdfLocation)); + ModelState.Remove(nameof(ProgramVideo.PdfName)); + + if (ModelState.IsValid) + { + if (pdfFile != null) + { + try + { + string filePath = await _azureBlobUploader.UploadFileAsync(pdfFile, pdfFile.FileName); + programVideo.PdfLocation = filePath; + programVideo.PdfName = pdfFile.FileName; + } + catch (Exception ex) + { + TempData["Message"] = $"Error uploading PDF: {ex.Message}"; + return View(programVideo); + } + } + + await ProgramVideoDB.AddAsync(_context, programVideo); + TempData["Message"] = $"Video \"{programVideo.Title}\" added successfully"; + return RedirectToAction("ManageVideos"); + } + + return View(programVideo); + } + + [HttpGet] + public async Task EditVideo(int id) + { + ProgramVideo? video = await ProgramVideoDB.GetVideoAsync(_context, id); + if (video == null) + { + return NotFound(); + } + return View(video); + } + + [HttpPost] + public async Task EditVideo(ProgramVideo programVideo, IFormFile? pdfFile) + { + ModelState.Remove(nameof(ProgramVideo.PdfLocation)); + ModelState.Remove(nameof(ProgramVideo.PdfName)); + + if (ModelState.IsValid) + { + if (pdfFile != null) + { + try + { + string filePath = await _azureBlobUploader.UploadFileAsync(pdfFile, pdfFile.FileName); + programVideo.PdfLocation = filePath; + programVideo.PdfName = pdfFile.FileName; + } + catch (Exception ex) + { + TempData["Message"] = $"Error uploading PDF: {ex.Message}"; + return View(programVideo); + } + } + + await ProgramVideoDB.UpdateAsync(_context, programVideo); + TempData["Message"] = $"Video \"{programVideo.Title}\" updated successfully"; + return RedirectToAction("ManageVideos"); + } + + return View(programVideo); + } + + [HttpGet] + public async Task DeleteVideo(int id) + { + ProgramVideo? video = await ProgramVideoDB.GetVideoAsync(_context, id); + if (video == null) + { + return NotFound(); + } + return View(video); + } + + [HttpPost] + [ActionName("DeleteVideo")] + public async Task ConfirmDeleteVideo(int id) + { + try + { + ProgramVideo? video = await ProgramVideoDB.GetVideoAsync(_context, id); + if (video != null) + { + if (!string.IsNullOrEmpty(video.PdfLocation)) + { + string? blobName = video.PdfLocation.Split('/').LastOrDefault(); + if (blobName != null) + { + await _azureBlobUploader.DeleteFileAsync(blobName); + } + } + + await ProgramVideoDB.DeleteAsync(_context, id); + TempData["Message"] = $"Video \"{video.Title}\" deleted successfully"; + } + } + catch (Exception ex) + { + TempData["Message"] = $"Error deleting video: {ex.Message}"; + } + + return RedirectToAction("ManageVideos"); + } } } diff --git a/PC2/Data/ApplicationDbContext.cs b/PC2/Data/ApplicationDbContext.cs index bddbf5ae..ea4704f3 100644 --- a/PC2/Data/ApplicationDbContext.cs +++ b/PC2/Data/ApplicationDbContext.cs @@ -42,6 +42,8 @@ protected override void ConfigureConventions(ModelConfigurationBuilder builder) public virtual DbSet NewsletterFile { get; set; } public virtual DbSet HousingProgram { get; set; } + + public virtual DbSet ProgramVideos { get; set; } } internal class DateOnlyConverter : ValueConverter diff --git a/PC2/Data/Migrations/20260301000112_AddProgramVideo.Designer.cs b/PC2/Data/Migrations/20260301000112_AddProgramVideo.Designer.cs new file mode 100644 index 00000000..63e38162 --- /dev/null +++ b/PC2/Data/Migrations/20260301000112_AddProgramVideo.Designer.cs @@ -0,0 +1,561 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PC2.Data; + +#nullable disable + +namespace PC2.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260301000112_AddProgramVideo")] + partial class AddProgramVideo + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AgencyAgencyCategory", b => + { + b.Property("AgenciesAgencyId") + .HasColumnType("int"); + + b.Property("AgencyCategoriesAgencyCategoryId") + .HasColumnType("int"); + + b.HasKey("AgenciesAgencyId", "AgencyCategoriesAgencyCategoryId"); + + b.HasIndex("AgencyCategoriesAgencyCategoryId"); + + b.ToTable("AgencyAgencyCategory"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("PC2.Models.Agency", b => + { + b.Property("AgencyId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AgencyId")); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("AgencyName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AgencyName2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("Contact") + .HasColumnType("nvarchar(max)"); + + b.Property("CrisisHelpHotline") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("Fax") + .HasColumnType("nvarchar(max)"); + + b.Property("MailingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("TDD") + .HasColumnType("nvarchar(max)"); + + b.Property("TTY") + .HasColumnType("nvarchar(max)"); + + b.Property("TollFree") + .HasColumnType("nvarchar(max)"); + + b.Property("Website") + .HasColumnType("nvarchar(max)"); + + b.Property("Zip") + .HasColumnType("nvarchar(max)"); + + b.HasKey("AgencyId"); + + b.ToTable("Agency"); + }); + + modelBuilder.Entity("PC2.Models.AgencyCategory", b => + { + b.Property("AgencyCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AgencyCategoryId")); + + b.Property("AgencyCategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("AgencyCategoryId"); + + b.ToTable("AgencyCategory"); + }); + + modelBuilder.Entity("PC2.Models.CalendarEvent", b => + { + b.Property("CalendarEventID") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CalendarEventID")); + + b.Property("CountyEvent") + .HasColumnType("bit"); + + b.Property("DateOfEvent") + .HasColumnType("date"); + + b.Property("EndingTime") + .HasColumnType("time"); + + b.Property("EventDescription") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PC2Event") + .HasColumnType("bit"); + + b.Property("StartingTime") + .HasColumnType("time"); + + b.HasKey("CalendarEventID"); + + b.ToTable("CalendarEvents"); + }); + + modelBuilder.Entity("PC2.Models.HousingProgram", b => + { + b.Property("HouseHoldSize") + .HasColumnType("int"); + + b.Property("LastUpdated") + .HasColumnType("datetime2"); + + b.Property("MaximumIncome") + .HasColumnType("float"); + + b.HasKey("HouseHoldSize"); + + b.ToTable("HousingProgram"); + }); + + modelBuilder.Entity("PC2.Models.NewsletterFile", b => + { + b.Property("NewsletterId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("NewsletterId")); + + b.Property("Location") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("NewsletterId"); + + b.ToTable("NewsletterFile"); + }); + + modelBuilder.Entity("PC2.Models.People", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ID")); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("nvarchar(21)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PriorityOrder") + .HasColumnType("tinyint"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("ID"); + + b.ToTable("People"); + + b.HasDiscriminator().HasValue("People"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("PC2.Models.ProgramVideo", b => + { + b.Property("ProgramVideoId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ProgramVideoId")); + + b.Property("PdfLocation") + .HasColumnType("nvarchar(max)"); + + b.Property("PdfName") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("YouTubeVideoId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("ProgramVideoId"); + + b.ToTable("ProgramVideos"); + }); + + modelBuilder.Entity("PC2.Models.Board", b => + { + b.HasBaseType("PC2.Models.People"); + + b.Property("MembershipStart") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasDiscriminator().HasValue("Board"); + }); + + modelBuilder.Entity("PC2.Models.Staff", b => + { + b.HasBaseType("PC2.Models.People"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Extension") + .HasColumnType("int"); + + b.Property("Phone") + .HasColumnType("nvarchar(max)"); + + b.HasDiscriminator().HasValue("Staff"); + }); + + modelBuilder.Entity("PC2.Models.SteeringCommittee", b => + { + b.HasBaseType("PC2.Models.People"); + + b.HasDiscriminator().HasValue("SteeringCommittee"); + }); + + modelBuilder.Entity("AgencyAgencyCategory", b => + { + b.HasOne("PC2.Models.Agency", null) + .WithMany() + .HasForeignKey("AgenciesAgencyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PC2.Models.AgencyCategory", null) + .WithMany() + .HasForeignKey("AgencyCategoriesAgencyCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", 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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/PC2/Data/Migrations/20260301000112_AddProgramVideo.cs b/PC2/Data/Migrations/20260301000112_AddProgramVideo.cs new file mode 100644 index 00000000..bd471d22 --- /dev/null +++ b/PC2/Data/Migrations/20260301000112_AddProgramVideo.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PC2.Data.Migrations +{ + /// + public partial class AddProgramVideo : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ProgramVideos", + columns: table => new + { + ProgramVideoId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Title = table.Column(type: "nvarchar(max)", nullable: false), + YouTubeVideoId = table.Column(type: "nvarchar(max)", nullable: false), + PdfLocation = table.Column(type: "nvarchar(max)", nullable: true), + PdfName = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ProgramVideos", x => x.ProgramVideoId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProgramVideos"); + } + } +} diff --git a/PC2/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/PC2/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 9415dce2..bab22d15 100644 --- a/PC2/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/PC2/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("ProductVersion", "10.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -425,6 +425,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.UseTphMappingStrategy(); }); + modelBuilder.Entity("PC2.Models.ProgramVideo", b => + { + b.Property("ProgramVideoId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ProgramVideoId")); + + b.Property("PdfLocation") + .HasColumnType("nvarchar(max)"); + + b.Property("PdfName") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("YouTubeVideoId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("ProgramVideoId"); + + b.ToTable("ProgramVideos"); + }); + modelBuilder.Entity("PC2.Models.Board", b => { b.HasBaseType("PC2.Models.People"); diff --git a/PC2/Data/ProgramVideoDB.cs b/PC2/Data/ProgramVideoDB.cs new file mode 100644 index 00000000..7deb606e --- /dev/null +++ b/PC2/Data/ProgramVideoDB.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using PC2.Models; + +namespace PC2.Data; + +public class ProgramVideoDB +{ + public static async Task AddAsync(ApplicationDbContext context, ProgramVideo programVideo) + { + context.ProgramVideos.Add(programVideo); + await context.SaveChangesAsync(); + } + + public static async Task> GetAllAsync(ApplicationDbContext context) + { + return await (from pv in context.ProgramVideos + select pv).ToListAsync(); + } + + public static async Task GetVideoAsync(ApplicationDbContext context, int id) + { + return await context.ProgramVideos.FindAsync(id); + } + + public static async Task UpdateAsync(ApplicationDbContext context, ProgramVideo programVideo) + { + context.ProgramVideos.Update(programVideo); + await context.SaveChangesAsync(); + } + + public static async Task DeleteAsync(ApplicationDbContext context, int id) + { + ProgramVideo? programVideo = await context.ProgramVideos.FindAsync(id); + if (programVideo != null) + { + context.ProgramVideos.Remove(programVideo); + await context.SaveChangesAsync(); + } + } +} diff --git a/PC2/Models/ProgramVideo.cs b/PC2/Models/ProgramVideo.cs new file mode 100644 index 00000000..f4dbeafb --- /dev/null +++ b/PC2/Models/ProgramVideo.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; + +namespace PC2.Models +{ + /// + /// Represents a program video entry + /// + public class ProgramVideo + { + /// + /// The unique identifier for the program video + /// + [Key] + public int ProgramVideoId { get; set; } + + /// + /// The display title of the video + /// + [Required] + public string Title { get; set; } = null!; + + /// + /// The YouTube video ID (e.g. "tRadgduogZg") + /// + [Required] + public string YouTubeVideoId { get; set; } = null!; + + /// + /// Optional URL to a PDF attachment stored in Azure Blob Storage + /// + public string? PdfLocation { get; set; } + + /// + /// Optional display name for the PDF attachment + /// + public string? PdfName { get; set; } + } +} diff --git a/PC2/Views/ProgramVideos/CreateVideo.cshtml b/PC2/Views/ProgramVideos/CreateVideo.cshtml new file mode 100644 index 00000000..1327ad82 --- /dev/null +++ b/PC2/Views/ProgramVideos/CreateVideo.cshtml @@ -0,0 +1,45 @@ +@model PC2.Models.ProgramVideo + +@{ + ViewData["Title"] = "Add Program Video"; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ +
+ + + +
+ +
+ + + + Enter the YouTube video ID or paste a full YouTube URL — the ID will be extracted automatically. +
+ +
+ + + +
+ +
+ + Cancel +
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + +} diff --git a/PC2/Views/ProgramVideos/DeleteVideo.cshtml b/PC2/Views/ProgramVideos/DeleteVideo.cshtml new file mode 100644 index 00000000..0f470c29 --- /dev/null +++ b/PC2/Views/ProgramVideos/DeleteVideo.cshtml @@ -0,0 +1,29 @@ +@model PC2.Models.ProgramVideo + +@{ + ViewData["Title"] = "Delete Program Video"; +} + +

Delete "@Model.Title"

+ +

Are you sure you want to delete "@Model.Title"?

+
+
+
+
Title
+
@Model.Title
+
YouTube Video ID
+
@Model.YouTubeVideoId
+ @if (!string.IsNullOrEmpty(Model.PdfLocation)) + { +
PDF Attachment
+
@Model.PdfName
+ } +
+ +
+ + | + Back to List +
+
diff --git a/PC2/Views/ProgramVideos/EditVideo.cshtml b/PC2/Views/ProgramVideos/EditVideo.cshtml new file mode 100644 index 00000000..9f09eea4 --- /dev/null +++ b/PC2/Views/ProgramVideos/EditVideo.cshtml @@ -0,0 +1,55 @@ +@model PC2.Models.ProgramVideo + +@{ + ViewData["Title"] = "Edit Program Video"; +} + +

@ViewData["Title"]

+ +
+
+
+
+
+ + + + +
+ + + +
+ +
+ + + + Enter the YouTube video ID or paste a full YouTube URL — the ID will be extracted automatically. +
+ +
+ + @if (!string.IsNullOrEmpty(Model.PdfLocation)) + { +

+ Current: @Model.PdfName +

+ } + + + Upload a new PDF to replace the existing attachment, or leave blank to keep the current one. +
+ +
+ + Cancel +
+
+
+
+ +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + +} diff --git a/PC2/Views/ProgramVideos/Index.cshtml b/PC2/Views/ProgramVideos/Index.cshtml index 9109b5c6..d7c61ee3 100644 --- a/PC2/Views/ProgramVideos/Index.cshtml +++ b/PC2/Views/ProgramVideos/Index.cshtml @@ -1,4 +1,6 @@ -@section head { +@model IEnumerable + +@section head { } @@ -23,91 +28,74 @@
-
-
-

Estate Planning

-
-
-
-
-
-

Guardianships

-
-
-
-
-
-

Special Needs Trusts & Able Accounts

+@if (Model != null && Model.Any()) +{ +
+ @foreach (var video in Model) + { +
+
+

@video.Title

+
+
+ @if (!string.IsNullOrEmpty(video.PdfLocation)) + { +
+
+
+
+
+ +
+
@video.PdfName
+ PDF Handout +
+
+ +
+
+
+
+ } +
+
+ }
-
-
- \ No newline at end of file + +} +else +{ +

No videos are currently available.

+} diff --git a/PC2/Views/ProgramVideos/ManageVideos.cshtml b/PC2/Views/ProgramVideos/ManageVideos.cshtml new file mode 100644 index 00000000..c8c8c8f9 --- /dev/null +++ b/PC2/Views/ProgramVideos/ManageVideos.cshtml @@ -0,0 +1,55 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Manage Program Videos"; +} + +

@ViewData["Title"]

+ +@{ + if (TempData["Message"] != null) + { + + } +} + +

+ Add New Video +

+ + + + + + + + + + + + @foreach (var video in Model) + { + + + + + + + } + +
TitleYouTube Video IDPDF Attachment
@video.Title@video.YouTubeVideoId + @if (!string.IsNullOrEmpty(video.PdfLocation)) + { + @video.PdfName + } + else + { + None + } + + Edit | + Delete +
diff --git a/PC2/Views/Shared/_Layout.cshtml b/PC2/Views/Shared/_Layout.cshtml index dcfa0d3c..1b9e4f69 100644 --- a/PC2/Views/Shared/_Layout.cshtml +++ b/PC2/Views/Shared/_Layout.cshtml @@ -109,6 +109,7 @@
  • View Steering Committee
  • Create Steering Committee Member
  • Upload Newsletter
  • +
  • Program Videos
  • } diff --git a/PC2/wwwroot/js/program-videos.js b/PC2/wwwroot/js/program-videos.js new file mode 100644 index 00000000..d523cd65 --- /dev/null +++ b/PC2/wwwroot/js/program-videos.js @@ -0,0 +1,47 @@ +function extractYouTubeId(value) { + // Matches full YouTube URLs: youtube.com/watch?v=ID, youtu.be/ID, youtube.com/embed/ID, youtube.com/v/ID + var patterns = [ + /[?&]v=([a-zA-Z0-9_-]{11,})/, + /youtu\.be\/([a-zA-Z0-9_-]{11,})/, + /\/embed\/([a-zA-Z0-9_-]{11,})/, + /\/v\/([a-zA-Z0-9_-]{11,})/ + ]; + for (var i = 0; i < patterns.length; i++) { + var match = value.match(patterns[i]); + if (match) return match[1]; + } + return null; +} + +function checkFileSize() { + var fileInput = document.getElementById('pdfFile'); + var fileSizeError = document.getElementById('fileSizeError'); + var submitButton = document.getElementById('submitButton'); + var maxSize = 50 * 1024 * 1024; // 50MB + + if (fileInput.files.length > 0) { + var file = fileInput.files[0]; + if (file.size > maxSize) { + fileSizeError.textContent = "The file size exceeds the 50MB limit."; + submitButton.disabled = true; + } else { + fileSizeError.textContent = ""; + submitButton.disabled = false; + } + } else { + fileSizeError.textContent = ""; + submitButton.disabled = false; + } +} + +document.addEventListener('DOMContentLoaded', function () { + var input = document.getElementById('youTubeVideoId'); + if (input) { + input.addEventListener('blur', function () { + var extracted = extractYouTubeId(this.value.trim()); + if (extracted) { + this.value = extracted; + } + }); + } +}); diff --git a/PC2Tests/Data/ProgramVideoDBTests.cs b/PC2Tests/Data/ProgramVideoDBTests.cs new file mode 100644 index 00000000..85df3f50 --- /dev/null +++ b/PC2Tests/Data/ProgramVideoDBTests.cs @@ -0,0 +1,208 @@ +using Microsoft.EntityFrameworkCore; +using PC2.Data; +using PC2.Models; + +namespace PC2Tests.Data; + +[TestClass] +public class ProgramVideoDBTests +{ + private ApplicationDbContext _context = null!; + private DbContextOptions _options = null!; + + [TestInitialize] + public void Setup() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _context = new ApplicationDbContext(_options); + } + + [TestCleanup] + public void Cleanup() + { + _context.Database.EnsureDeleted(); + _context.Dispose(); + } + + #region AddAsync Tests + + [TestMethod] + public async Task AddAsync_WithValidVideo_AddsToDatabase() + { + // Arrange + var video = new ProgramVideo + { + Title = "Estate Planning", + YouTubeVideoId = "tRadgduogZg" + }; + + // Act + await ProgramVideoDB.AddAsync(_context, video); + + // Assert + var result = await _context.ProgramVideos.ToListAsync(); + Assert.HasCount(1, result); + Assert.AreEqual("Estate Planning", result[0].Title); + Assert.AreEqual("tRadgduogZg", result[0].YouTubeVideoId); + } + + [TestMethod] + public async Task AddAsync_WithPdfAttachment_AddsToDatabase() + { + // Arrange + var video = new ProgramVideo + { + Title = "Guardianships", + YouTubeVideoId = "qUq_26jU2kU", + PdfLocation = "https://example.blob.core.windows.net/files/handout.pdf", + PdfName = "handout.pdf" + }; + + // Act + await ProgramVideoDB.AddAsync(_context, video); + + // Assert + var result = await _context.ProgramVideos.FindAsync(video.ProgramVideoId); + Assert.IsNotNull(result); + Assert.AreEqual("https://example.blob.core.windows.net/files/handout.pdf", result.PdfLocation); + Assert.AreEqual("handout.pdf", result.PdfName); + } + + #endregion + + #region GetAllAsync Tests + + [TestMethod] + public async Task GetAllAsync_WithNoVideos_ReturnsEmptyList() + { + // Act + var result = await ProgramVideoDB.GetAllAsync(_context); + + // Assert + Assert.IsNotNull(result); + Assert.IsEmpty(result); + } + + [TestMethod] + public async Task GetAllAsync_WithMultipleVideos_ReturnsAll() + { + // Arrange + var video1 = new ProgramVideo { Title = "Video 1", YouTubeVideoId = "abc123" }; + var video2 = new ProgramVideo { Title = "Video 2", YouTubeVideoId = "def456" }; + _context.ProgramVideos.AddRange(video1, video2); + await _context.SaveChangesAsync(); + + // Act + var result = await ProgramVideoDB.GetAllAsync(_context); + + // Assert + Assert.HasCount(2, result); + } + + #endregion + + #region GetVideoAsync Tests + + [TestMethod] + public async Task GetVideoAsync_WithValidId_ReturnsVideo() + { + // Arrange + var video = new ProgramVideo { Title = "Estate Planning", YouTubeVideoId = "tRadgduogZg" }; + _context.ProgramVideos.Add(video); + await _context.SaveChangesAsync(); + + // Act + var result = await ProgramVideoDB.GetVideoAsync(_context, video.ProgramVideoId); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual("Estate Planning", result.Title); + } + + [TestMethod] + public async Task GetVideoAsync_WithInvalidId_ReturnsNull() + { + // Act + var result = await ProgramVideoDB.GetVideoAsync(_context, 999); + + // Assert + Assert.IsNull(result); + } + + #endregion + + #region UpdateAsync Tests + + [TestMethod] + public async Task UpdateAsync_WithValidChanges_UpdatesDatabase() + { + // Arrange + var video = new ProgramVideo { Title = "Old Title", YouTubeVideoId = "abc123" }; + _context.ProgramVideos.Add(video); + await _context.SaveChangesAsync(); + + _context.Entry(video).State = EntityState.Detached; + video.Title = "New Title"; + video.YouTubeVideoId = "xyz789"; + + // Act + await ProgramVideoDB.UpdateAsync(_context, video); + + // Assert + var result = await _context.ProgramVideos.FindAsync(video.ProgramVideoId); + Assert.IsNotNull(result); + Assert.AreEqual("New Title", result.Title); + Assert.AreEqual("xyz789", result.YouTubeVideoId); + } + + #endregion + + #region DeleteAsync Tests + + [TestMethod] + public async Task DeleteAsync_WithValidId_RemovesFromDatabase() + { + // Arrange + var video = new ProgramVideo { Title = "Estate Planning", YouTubeVideoId = "tRadgduogZg" }; + _context.ProgramVideos.Add(video); + await _context.SaveChangesAsync(); + int id = video.ProgramVideoId; + + // Act + await ProgramVideoDB.DeleteAsync(_context, id); + + // Assert + var result = await _context.ProgramVideos.FindAsync(id); + Assert.IsNull(result); + } + + [TestMethod] + public async Task DeleteAsync_WithInvalidId_DoesNotThrow() + { + // Act & Assert - should not throw + await ProgramVideoDB.DeleteAsync(_context, 999); + } + + [TestMethod] + public async Task DeleteAsync_DeletesOnlySpecifiedVideo() + { + // Arrange + var video1 = new ProgramVideo { Title = "Video 1", YouTubeVideoId = "abc123" }; + var video2 = new ProgramVideo { Title = "Video 2", YouTubeVideoId = "def456" }; + _context.ProgramVideos.AddRange(video1, video2); + await _context.SaveChangesAsync(); + + // Act + await ProgramVideoDB.DeleteAsync(_context, video1.ProgramVideoId); + + // Assert + var remaining = await _context.ProgramVideos.ToListAsync(); + Assert.HasCount(1, remaining); + Assert.AreEqual("Video 2", remaining[0].Title); + } + + #endregion +}