Skip to content

Staff photos#470

Merged
JoeProgrammer88 merged 25 commits intomainfrom
StaffPhotos
Mar 3, 2026
Merged

Staff photos#470
JoeProgrammer88 merged 25 commits intomainfrom
StaffPhotos

Conversation

@JoeProgrammer88
Copy link
Copy Markdown
Member

@JoeProgrammer88 JoeProgrammer88 commented Feb 19, 2026

Closes #370

Summary

Add the ability for the client to manage photos for the staff members

Copilot Summary

This pull request introduces a major refactor for managing staff, board, and steering committee members by consolidating their CRUD operations into a new, unified PeopleController. Additionally, it adds support for storing and managing profile images for these members, including database schema changes and file handling logic. Several redundant methods are removed from AboutController, and the code is streamlined for clarity and maintainability.

Refactor and consolidation of person management:

  • Added a new PeopleController that centralizes CRUD operations for staff, board, and steering committee members, replacing multiple methods previously in AboutController. This controller also introduces unified view models and type-specific validation. (PC2/Controllers/PeopleController.cs)
  • Removed all staff, board, and steering committee CRUD methods from AboutController, shifting their responsibilities to PeopleController. (PC2/Controllers/AboutController.cs)

Profile image support:

  • Added logic to handle uploading, resizing, and deleting profile images for people, including integration with Azure Blob Storage and image validation. (PC2/Controllers/PeopleController.cs)
  • Introduced a new database migration to add an ImageUrl column to the People table, enabling storage of image links for staff, board, and steering committee members. (PC2/Data/Migrations/20250807162448_AddImageUrlToStaff.cs)
  • Updated the Entity Framework model snapshot to reflect the new ImageUrl column in the People table. (PC2/Data/Migrations/ApplicationDbContextModelSnapshot.cs)

Minor code clean-up:

  • Removed unnecessary comments and streamlined logic in newsletter file upload and deletion methods in AboutController. (PC2/Controllers/AboutController.cs) [1] [2] [3] [4]
  • Minor improvements to data handling and validation messaging in housing program methods. (PC2/Controllers/AboutController.cs) [1] [2]

JoeProgrammer88 and others added 18 commits August 8, 2025 16:14
- Updated AboutController to include ImageService for handling image uploads and resizing.
- Introduced CreateStaffViewModel and EditStaffViewModel for managing staff data, including image URLs.
- Added ImageUrl property to Staff entity and updated ApplicationDbContextModelSnapshot.
- Initialized Staff, Board, and SteeringCommittee lists in AboutUsModel.
- Registered ImageService in Program.cs for dependency injection.
- Updated CreateStaff and EditStaff views to support file uploads and validation.
- Enhanced About.cshtml layout to display staff with images using Bootstrap cards.
- Added PeopleController for CRUD operations on staff with authorization checks.
- Created migration files to add ImageUrl column to People table.
- Added FormFileFromStream and ImageService classes for image processing.
- Created StaffViewModels for managing staff creation and editing.
- Updated/Create views (Create.cshtml, Delete.cshtml, Details.cshtml, Edit.cshtml, Index.cshtml) to support new functionality.
Reduced #heading top margin and switched to em units for consistency. Removed #pc2Logo padding-bottom to eliminate extra space below the logo on large screens.
Moved Staff, Board, and Steering Committee CRUD actions from AboutController to a new PeopleController. Updated dependency injection for image services. Staff photo upload now requires a file (no URL option). Rewrote and reorganized all related Razor views, removing generic People views. Updated navigation to use new controller routes. Cleaned up legacy code and improved UI consistency.
ImageUrl is now defined in the People base class, allowing all People entities to have an associated image URL. The property retains its DataType attribute for URL validation.
Refactored the management of Staff, Board, and Steering Committee members into a single, generic CRUD interface using a new PersonType enum and PersonViewModel. Replaced all separate controller actions and views with unified, type-driven implementations. Updated navigation and removed old, redundant ViewModels. This reduces duplication, simplifies maintenance, and provides a consistent UI for all people management.
Relaxed staff photo upload validation in controller and views.
Generalized image filename generation for all person types.
Added client-side image preview to Create/Edit views.
Show current image in Delete view.
Updated Edit view to clear preview when removing photo.
Removed "Required for staff" note from Create view.
Renamed .staff-avatar to .person-avatar for consistency. Board and steering committee members with images now use card layouts; those without images are listed in tables. Section headers adjust dynamically based on image presence. Index page adds avatar column with image or placeholder icon for each person. Enhances visual clarity and consistency across the site.
Changed PriorityOrder input to hidden for SteeringCommittee (set to 1). For other types, replaced number input with select options. Staff shows "Director" and "Other"; others show "Board Chair", "Treasurer", and "Other".
Replaced console error output with ILogger-based logging for image upload and deletion errors. Updated image resizing to use explicit 250x250 dimensions during upload. Injected ILogger<PeopleController> via constructor.
Updated the PeopleController to resize uploaded images to 350x350 pixels instead of 250x250 before uploading. This change ensures higher resolution profile images.
Deleted staff image section in About.cshtml and removed all associated image files from the project.
Added calls to RemovePersonPhoto for Staff, Board, and Steering Committee members to ensure their photos are deleted from storage before their database records are removed.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements photo management capabilities for staff, board, and steering committee members, addressing issue #370. The changes consolidate the previously separate CRUD operations for different member types into a unified PeopleController, add image upload/resize/delete functionality using Azure Blob Storage, and update the public-facing About page to display member photos in a modern card-based layout.

Changes:

  • Consolidated staff/board/steering committee CRUD operations into a new PeopleController with a unified PersonViewModel
  • Added photo upload functionality with automatic resizing using ImageSharp library
  • Updated the About page to display member photos using Bootstrap cards
  • Added database migration to support ImageUrl storage for all person types

Reviewed changes

Copilot reviewed 29 out of 40 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
PC2/Controllers/PeopleController.cs New unified controller for managing all person types with photo upload/delete functionality
PC2/Controllers/AboutController.cs Removed staff/board/committee CRUD methods (migrated to PeopleController)
PC2/Models/ViewModels/PersonViewModel.cs New unified view model for all person types with photo support
PC2/Models/People.cs Added ImageUrl property to base People class and PersonType enum
PC2/Models/FormFileFromStream.cs Helper class to wrap streams as IFormFile for Azure upload
PC2/Services/ImageService.cs New service for image processing, resizing, and validation
PC2/Views/People/*.cshtml New consolidated CRUD views for all person types
PC2/Views/Home/About.cshtml Updated to display photos in card layout with fallback for members without photos
PC2/Views/Shared/_Layout.cshtml Updated navigation to use new PeopleController routes
PC2/Views/About/*.cshtml Removed old separate CRUD views for staff/board/committee
PC2/Data/Migrations/20250807162448_AddImageUrlToStaff.cs Database migration adding ImageUrl column to People table
PC2/Program.cs Registered ImageService in DI container
PC2/PC2.csproj Added SixLabors.ImageSharp package dependency
PC2/wwwroot/css/site.css Minor CSS adjustments for layout
Files not reviewed (1)
  • PC2/Data/Migrations/20250807162448_AddImageUrlToStaff.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +63
await image.SaveAsJpegAsync(outputStream, new JpegEncoder
{
Quality = 85
});
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image is always converted to JPEG format regardless of the input format (line 60). While this ensures consistency, it means that PNG images with transparency will lose their alpha channel and may have unexpected visual results. Consider either preserving the original format or documenting this behavior clearly in the UI helper text.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +121
/// <summary>
/// Validates if the uploaded file is a valid image
/// </summary>
public static bool IsValidImageFile(IFormFile file)
{
if (file == null || file.Length == 0)
return false;

var allowedMimeTypes = new[]
{
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/bmp",
"image/webp"
};

return allowedMimeTypes.Contains(file.ContentType?.ToLower());
}

/// <summary>
/// Gets a safe filename for uploaded images
/// </summary>
public static string GetSafeImageFileName(string originalFileName, int personId)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image validation only checks the MIME type from the Content-Type header, which can be easily spoofed by attackers. This could potentially allow malicious files to be uploaded. Consider adding additional validation such as checking the actual file signature (magic bytes) or using ImageSharp to attempt to load the image and catching any exceptions, which would verify it's actually a valid image file.

Suggested change
/// <summary>
/// Validates if the uploaded file is a valid image
/// </summary>
public static bool IsValidImageFile(IFormFile file)
{
if (file == null || file.Length == 0)
return false;
var allowedMimeTypes = new[]
{
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/bmp",
"image/webp"
};
return allowedMimeTypes.Contains(file.ContentType?.ToLower());
}
/// <summary>
/// Gets a safe filename for uploaded images
/// </summary>
public static string GetSafeImageFileName(string originalFileName, int personId)
/// <summary>
/// Validates if the uploaded file is a valid image
/// </summary>
public static bool IsValidImageFile(IFormFile file)
{
if (file == null || file.Length == 0)
return false;
var allowedMimeTypes = new[]
{
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/bmp",
"image/webp"
};
var contentType = file.ContentType?.ToLowerInvariant();
if (string.IsNullOrWhiteSpace(contentType) || !allowedMimeTypes.Contains(contentType))
return false;
// Attempt to load the file as an image using ImageSharp to verify it is a valid image file
try
{
using var stream = file.OpenReadStream();
using var image = Image.Load(stream);
return true;
}
catch (UnknownImageFormatException)
{
return false;
}
catch (ImageFormatException)
{
return false;
}
catch
{
// Any error while loading the image indicates an invalid or corrupted image file
return false;
}
}
/// <summary>
/// Gets a safe filename for uploaded images
/// </summary>
public static string GetSafeImageFileName(string originalFileName, int personId)

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +28
[Display(Name = "Photo")]
public IFormFile? PhotoFile { get; set; }
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PhotoFile property lacks file size validation attributes. Without a maximum file size limit, users could upload extremely large files which could cause performance issues, timeout errors, or consume excessive bandwidth and storage. Consider adding a file size validation attribute or documenting the maximum acceptable file size.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Controllers/PeopleController.cs Outdated
await RemovePersonPhoto(person);

var safeFileName = ImageService.GetSafeImageFileName(photoFile.FileName, personId ?? 0);
using var resizedImageStream = await _imageService.ResizeImageAsync(photoFile.OpenReadStream(), 350, 350);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resizedImageStream is wrapped in a using statement, but it's then wrapped in a FormFileFromStream and passed to UploadFileAsync. The UploadFileAsync method calls OpenReadStream() which returns the same stream instance, and then disposes it in another using statement (line 43-46 of AzureBlobUploader.cs). This means the stream is disposed twice - once inside UploadFileAsync and once when the HandlePhotoUpload method's using block exits. This could cause ObjectDisposedException or other unexpected behavior. Consider either removing the using statement from HandlePhotoUpload or ensuring the stream isn't disposed in UploadFileAsync.

Suggested change
using var resizedImageStream = await _imageService.ResizeImageAsync(photoFile.OpenReadStream(), 350, 350);
var resizedImageStream = await _imageService.ResizeImageAsync(photoFile.OpenReadStream(), 350, 350);

Copilot uses AI. Check for mistakes.
Comment thread PC2/Models/People.cs
/// <summary>
/// URL to the person's photo/image.
/// </summary>
[DataType(DataType.Url)]
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ImageUrl property has a DataType.Url attribute for display purposes but lacks a [Url] validation attribute. This means invalid URL formats could be stored in the database and rendered in views, potentially causing broken images or, in edge cases, unexpected behavior. Consider adding a [Url] validation attribute to ensure only valid URLs are stored.

Suggested change
[DataType(DataType.Url)]
[DataType(DataType.Url)]
[Url]

Copilot uses AI. Check for mistakes.
Comment thread PC2/Models/FormFileFromStream.cs Outdated
Comment on lines +1 to +43
namespace PC2.Models
{
/// <summary>
/// Implementation of IFormFile that wraps a Stream
/// Used for creating IFormFile from resized image streams
/// </summary>
public class FormFileFromStream : IFormFile
{
private readonly Stream _stream;
private readonly string _name;
private readonly string _contentType;

public FormFileFromStream(Stream stream, string name, string contentType)
{
_stream = stream;
_name = name;
_contentType = contentType;
}

public string ContentType => _contentType;
public string ContentDisposition => $"form-data; name=\"{Name}\"; filename=\"{FileName}\"";
public IHeaderDictionary Headers => new HeaderDictionary();
public long Length => _stream.Length;
public string Name => _name;
public string FileName => _name;

public void CopyTo(Stream target)
{
_stream.CopyTo(target);
}

public Task CopyToAsync(Stream target, CancellationToken cancellationToken = default)
{
return _stream.CopyToAsync(target, cancellationToken);
}

public Stream OpenReadStream()
{
_stream.Position = 0;
return _stream;
}
}
} No newline at end of file
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FormFileFromStream class wraps a Stream but doesn't implement IDisposable. The wrapped stream is never disposed, which could lead to resource leaks. Since IFormFile doesn't inherit from IDisposable, the consuming code may not know to dispose the underlying stream. Consider either implementing IDisposable or documenting that the caller is responsible for disposing the original stream that was passed to the constructor.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Controllers/PeopleController.cs Outdated
Comment on lines +183 to +209
[HttpPost, ActionName("Delete")]
public async Task<IActionResult> ConfirmDelete(int id, PersonType type)
{
switch (type)
{
case PersonType.Staff:
var staff = await StaffDB.GetStaffMember(_context, id);
if (staff == null) return NotFound();
await RemovePersonPhoto(staff);
await StaffDB.Delete(_context, staff);
break;

case PersonType.Board:
var board = await BoardDB.GetBoardMember(_context, id);
if (board == null) return NotFound();
await RemovePersonPhoto(board);
await BoardDB.Delete(_context, board);
break;

case PersonType.SteeringCommittee:
var sc = await SteeringCommitteeDB.GetSteeringCommitteeMember(_context, id);
if (sc == null) return NotFound();
await RemovePersonPhoto(sc);
await SteeringCommitteeDB.Delete(_context, sc);
break;
}
return RedirectToAction(nameof(Index), new { type });
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If RemovePersonPhoto fails during person deletion (line 191, 198, or 205), the exception is caught and logged in RemovePersonPhoto (line 253-256), but the person is still deleted from the database. This results in orphaned blobs in Azure storage that can never be cleaned up since the reference to them is lost. Consider either: 1) Propagating the exception to prevent database deletion if blob deletion fails, or 2) Implementing a separate cleanup process for orphaned blobs, or 3) At minimum, logging a warning that manual cleanup may be needed.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +28
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace PC2.Data.Migrations
{
/// <inheritdoc />
public partial class AddImageUrlToStaff : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ImageUrl",
table: "People",
type: "nvarchar(max)",
nullable: true);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ImageUrl",
table: "People");
}
}
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration is named "AddImageUrlToStaff" but it actually adds the ImageUrl column to the People table (the base table in the TPH hierarchy), which affects all person types including Staff, Board, and SteeringCommittee. While functionally correct, the name is misleading. Consider a more accurate name like "AddImageUrlToPeople" to better reflect what the migration actually does.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +73
public async Task<MemoryStream> ResizeImageAsync(Stream imageStream, int maxWidth = 800, int maxHeight = 600)
{
try
{
using var image = await Image.LoadAsync(imageStream);

// Calculate new dimensions while maintaining aspect ratio
(int newWidth, int newHeight) = CalculateResizeDimensions(image.Width, image.Height, maxWidth, maxHeight);

// If image is already smaller than max dimensions, return original
if (newWidth == image.Width && newHeight == image.Height)
{
var originalStream = new MemoryStream();
imageStream.Position = 0;
await imageStream.CopyToAsync(originalStream);
originalStream.Position = 0;
return originalStream;
}

// Resize the image
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(newWidth, newHeight),
Mode = ResizeMode.Max, // Ensure it fits within max dimensions
Sampler = KnownResamplers.Lanczos3 // High quality resampling (more CPU intensive)
}));

// Save to memory stream
var outputStream = new MemoryStream();

// Use JPEG format with good quality for most cases
await image.SaveAsJpegAsync(outputStream, new JpegEncoder
{
Quality = 85
});

outputStream.Position = 0;
return outputStream;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error resizing image");
throw new InvalidOperationException("Failed to resize image", ex);
}
}
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no file size validation before attempting to resize the image. A user could upload an extremely large file (e.g., several hundred MB or GB), which would consume significant memory and CPU during the ImageSharp processing, potentially causing timeouts or out-of-memory errors. Consider adding a maximum file size check (e.g., 10-20 MB) before processing the image.

Copilot uses AI. Check for mistakes.
Comment thread PC2/Views/People/Index.cshtml Outdated
JoeProgrammer88 and others added 7 commits February 24, 2026 09:04
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Refactored PeopleController for clarity and maintainability: moved [Authorize] to class level, used constructor DI, split CRUD actions into separate GET/POST methods, and improved type handling with switch expressions.
- Improved photo upload and deletion logic with better error handling and logging.
- Promoted FormFileFromStream and PersonViewModel to top-level classes with cleaner formatting and documentation.
- Cleaned up About.cshtml and removed an unused using from AboutController.
Removed using statement around file stream in AzureBlobUploader. The method no longer disposes the stream after upload; instead, the caller is now responsible for managing the stream's lifetime. This change clarifies ownership and prevents premature disposal.
Wrap Create, Edit, and Delete POST actions in try-catch blocks to log exceptions and display user-friendly error messages. Simplify HandlePhotoUpload by removing its internal try-catch, allowing errors to propagate. Ensure RemovePersonPhoto logs and rethrows exceptions. These changes improve reliability and user feedback.
Changed the staff image class from 'rounded-circle' to 'rounded'
in About.cshtml, so images now have slightly rounded corners
instead of being fully circular. This improves visual consistency
with other image styles on the site.
@JoeProgrammer88 JoeProgrammer88 marked this pull request as ready for review March 3, 2026 22:15
@JoeProgrammer88 JoeProgrammer88 merged commit 8d373f2 into main Mar 3, 2026
1 check passed
@JoeProgrammer88 JoeProgrammer88 deleted the StaffPhotos branch March 3, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Staff member image to staff CRUD pages

2 participants