From 2b8a2b78cd83cc551ebbe4c64c64c73058eba84e Mon Sep 17 00:00:00 2001 From: Joseph Ortiz Date: Fri, 10 Oct 2025 10:53:24 -0700 Subject: [PATCH 01/14] Add Courses management for instructors Added a "Courses" navigation link in `_Layout.cshtml` visible only to authenticated users with the "Instructor" role. Implemented a `CoursesController` with full CRUD functionality, restricted to instructors via `[Authorize(Roles = "Instructor")]`. Created views (`Create.cshtml`, `Delete.cshtml`, `Details.cshtml`, `Edit.cshtml`, `Index.cshtml`) to manage courses, including forms for adding, editing, and deleting courses, as well as displaying course details and a list of all courses. --- .../Controllers/CoursesController.cs | 158 ++++++++++++++++++ SpeakingInBitsWeb/Views/Courses/Create.cshtml | 43 +++++ SpeakingInBitsWeb/Views/Courses/Delete.cshtml | 39 +++++ .../Views/Courses/Details.cshtml | 36 ++++ SpeakingInBitsWeb/Views/Courses/Edit.cshtml | 44 +++++ SpeakingInBitsWeb/Views/Courses/Index.cshtml | 47 ++++++ SpeakingInBitsWeb/Views/Shared/_Layout.cshtml | 6 + 7 files changed, 373 insertions(+) create mode 100644 SpeakingInBitsWeb/Controllers/CoursesController.cs create mode 100644 SpeakingInBitsWeb/Views/Courses/Create.cshtml create mode 100644 SpeakingInBitsWeb/Views/Courses/Delete.cshtml create mode 100644 SpeakingInBitsWeb/Views/Courses/Details.cshtml create mode 100644 SpeakingInBitsWeb/Views/Courses/Edit.cshtml create mode 100644 SpeakingInBitsWeb/Views/Courses/Index.cshtml diff --git a/SpeakingInBitsWeb/Controllers/CoursesController.cs b/SpeakingInBitsWeb/Controllers/CoursesController.cs new file mode 100644 index 0000000..b46e913 --- /dev/null +++ b/SpeakingInBitsWeb/Controllers/CoursesController.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.EntityFrameworkCore; +using SpeakingInBitsWeb.Data; +using SpeakingInBitsWeb.Models; + +namespace SpeakingInBitsWeb.Controllers; + +[Authorize(Roles = "Instructor")] +public class CoursesController : Controller +{ + private readonly ApplicationDbContext _context; + + public CoursesController(ApplicationDbContext context) + { + _context = context; + } + + // GET: Courses + public async Task Index() + { + return View(await _context.Courses.ToListAsync()); + } + + // GET: Courses/Details/5 + public async Task Details(int? id) + { + if (id == null) + { + return NotFound(); + } + + var course = await _context.Courses + .FirstOrDefaultAsync(m => m.Id == id); + if (course == null) + { + return NotFound(); + } + + return View(course); + } + + // GET: Courses/Create + public IActionResult Create() + { + return View(); + } + + // POST: Courses/Create + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Create([Bind("Id,Title,CourseCode,Description")] Course course) + { + if (ModelState.IsValid) + { + _context.Add(course); + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + return View(course); + } + + // GET: Courses/Edit/5 + public async Task Edit(int? id) + { + if (id == null) + { + return NotFound(); + } + + var course = await _context.Courses.FindAsync(id); + if (course == null) + { + return NotFound(); + } + return View(course); + } + + // POST: Courses/Edit/5 + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Edit(int id, [Bind("Id,Title,CourseCode,Description")] Course course) + { + if (id != course.Id) + { + return NotFound(); + } + + if (ModelState.IsValid) + { + try + { + _context.Update(course); + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!CourseExists(course.Id)) + { + return NotFound(); + } + else + { + throw; + } + } + return RedirectToAction(nameof(Index)); + } + return View(course); + } + + // GET: Courses/Delete/5 + public async Task Delete(int? id) + { + if (id == null) + { + return NotFound(); + } + + var course = await _context.Courses + .FirstOrDefaultAsync(m => m.Id == id); + if (course == null) + { + return NotFound(); + } + + return View(course); + } + + // POST: Courses/Delete/5 + [HttpPost, ActionName("Delete")] + [ValidateAntiForgeryToken] + public async Task DeleteConfirmed(int id) + { + var course = await _context.Courses.FindAsync(id); + if (course != null) + { + _context.Courses.Remove(course); + } + + await _context.SaveChangesAsync(); + return RedirectToAction(nameof(Index)); + } + + private bool CourseExists(int id) + { + return _context.Courses.Any(e => e.Id == id); + } +} diff --git a/SpeakingInBitsWeb/Views/Courses/Create.cshtml b/SpeakingInBitsWeb/Views/Courses/Create.cshtml new file mode 100644 index 0000000..f45472b --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Create.cshtml @@ -0,0 +1,43 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Create"; +} + +

Create

+ +

Course

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/SpeakingInBitsWeb/Views/Courses/Delete.cshtml b/SpeakingInBitsWeb/Views/Courses/Delete.cshtml new file mode 100644 index 0000000..8bce683 --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Delete.cshtml @@ -0,0 +1,39 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Delete"; +} + +

Delete

+ +

Are you sure you want to delete this?

+
+

Course

+
+
+
+ @Html.DisplayNameFor(model => model.Title) +
+
+ @Html.DisplayFor(model => model.Title) +
+
+ @Html.DisplayNameFor(model => model.CourseCode) +
+
+ @Html.DisplayFor(model => model.CourseCode) +
+
+ @Html.DisplayNameFor(model => model.Description) +
+
+ @Html.DisplayFor(model => model.Description) +
+
+ +
+ + | + Back to List +
+
diff --git a/SpeakingInBitsWeb/Views/Courses/Details.cshtml b/SpeakingInBitsWeb/Views/Courses/Details.cshtml new file mode 100644 index 0000000..c69eae6 --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Details.cshtml @@ -0,0 +1,36 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Details"; +} + +

Details

+ +
+

Course

+
+
+
+ @Html.DisplayNameFor(model => model.Title) +
+
+ @Html.DisplayFor(model => model.Title) +
+
+ @Html.DisplayNameFor(model => model.CourseCode) +
+
+ @Html.DisplayFor(model => model.CourseCode) +
+
+ @Html.DisplayNameFor(model => model.Description) +
+
+ @Html.DisplayFor(model => model.Description) +
+
+
+ diff --git a/SpeakingInBitsWeb/Views/Courses/Edit.cshtml b/SpeakingInBitsWeb/Views/Courses/Edit.cshtml new file mode 100644 index 0000000..7efd64d --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Edit.cshtml @@ -0,0 +1,44 @@ +@model SpeakingInBitsWeb.Models.Course + +@{ + ViewData["Title"] = "Edit"; +} + +

Edit

+ +

Course

+
+
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ + + +@section Scripts { + @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} +} diff --git a/SpeakingInBitsWeb/Views/Courses/Index.cshtml b/SpeakingInBitsWeb/Views/Courses/Index.cshtml new file mode 100644 index 0000000..86036b2 --- /dev/null +++ b/SpeakingInBitsWeb/Views/Courses/Index.cshtml @@ -0,0 +1,47 @@ +@model IEnumerable + +@{ + ViewData["Title"] = "Index"; +} + +

Index

+ +

+ Create New +

+ + + + + + + + + + +@foreach (var item in Model) { + + + + + + +} + +
+ @Html.DisplayNameFor(model => model.Title) + + @Html.DisplayNameFor(model => model.CourseCode) + + @Html.DisplayNameFor(model => model.Description) +
+ @Html.DisplayFor(modelItem => item.Title) + + @Html.DisplayFor(modelItem => item.CourseCode) + + @Html.DisplayFor(modelItem => item.Description) + + Edit | + Details | + Delete +
diff --git a/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml b/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml index 628e80b..d8e05b0 100644 --- a/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml +++ b/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml @@ -26,6 +26,12 @@ + @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Instructor")) + { + + } From 4c290ce0b869c3339eaeb5c63a1e531e64e516df Mon Sep 17 00:00:00 2001 From: Joseph Ortiz Date: Fri, 10 Oct 2025 11:00:04 -0700 Subject: [PATCH 02/14] Refactor role management to use centralized constants Replaced hardcoded role names with constants from a new `Roles` class to improve maintainability and consistency. Updated the `CoursesController` to use `Roles.Instructor` in the `[Authorize]` attribute. Modified `SeedData.cs` to use `Roles` constants for role creation and assignment. Updated `_Layout.cshtml` to use `Roles.Instructor` in role checks and added the necessary namespace import. Added a new `Roles.cs` file to define role constants for "Instructor" and "Student". --- .../Controllers/CoursesController.cs | 2 +- SpeakingInBitsWeb/Models/Roles.cs | 17 +++++++++++++++++ SpeakingInBitsWeb/Models/SeedData.cs | 4 ++-- SpeakingInBitsWeb/Views/Shared/_Layout.cshtml | 5 +++-- 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 SpeakingInBitsWeb/Models/Roles.cs diff --git a/SpeakingInBitsWeb/Controllers/CoursesController.cs b/SpeakingInBitsWeb/Controllers/CoursesController.cs index b46e913..510b5bc 100644 --- a/SpeakingInBitsWeb/Controllers/CoursesController.cs +++ b/SpeakingInBitsWeb/Controllers/CoursesController.cs @@ -11,7 +11,7 @@ namespace SpeakingInBitsWeb.Controllers; -[Authorize(Roles = "Instructor")] +[Authorize(Roles = Roles.Instructor)] public class CoursesController : Controller { private readonly ApplicationDbContext _context; diff --git a/SpeakingInBitsWeb/Models/Roles.cs b/SpeakingInBitsWeb/Models/Roles.cs new file mode 100644 index 0000000..ca0ba1c --- /dev/null +++ b/SpeakingInBitsWeb/Models/Roles.cs @@ -0,0 +1,17 @@ +namespace SpeakingInBitsWeb.Models; + +/// +/// Defines constant values for application roles. +/// +public static class Roles +{ + /// + /// Role name for instructors who can manage courses. + /// + public const string Instructor = "Instructor"; + + /// + /// Role name for students who can enroll in courses. + /// + public const string Student = "Student"; +} diff --git a/SpeakingInBitsWeb/Models/SeedData.cs b/SpeakingInBitsWeb/Models/SeedData.cs index 61db0f2..c1ffad1 100644 --- a/SpeakingInBitsWeb/Models/SeedData.cs +++ b/SpeakingInBitsWeb/Models/SeedData.cs @@ -13,7 +13,7 @@ public static class SeedData /// The role manager service. public static async Task CreateRolesAsync(RoleManager roleManager) { - string[] roles = ["Instructor", "Student"]; + string[] roles = [Roles.Instructor, Roles.Student]; foreach (var role in roles) { @@ -49,7 +49,7 @@ public static async Task CreateDefaultUserAsync(UserManager use if (result.Succeeded) { - await userManager.AddToRoleAsync(newUser, "Instructor"); + await userManager.AddToRoleAsync(newUser, Roles.Instructor); } } } diff --git a/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml b/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml index d8e05b0..8ac05ee 100644 --- a/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml +++ b/SpeakingInBitsWeb/Views/Shared/_Layout.cshtml @@ -1,4 +1,5 @@ - +@using SpeakingInBitsWeb.Models + @@ -26,7 +27,7 @@ - @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Instructor")) + @if (User.Identity?.IsAuthenticated == true && User.IsInRole(Roles.Instructor)) {