CSC/ECE 517 Fall 2025 - E2552. ProjectTopic and SignedUpTeam

From Expertiza_Wiki
Jump to navigation Jump to search

E 2552. Refactoring Topics and Enhancing Assignment Management

Introduction

Expertiza is an educational web application created and maintained by the joint efforts of the students and the faculty at NCSU. It’s an open source project developed on Ruby on Rails platform and its code is available on Github. It allows students to review each other’s work and improve their work upon this feedback. The current effort involves reimplementing the application with a Ruby on Rails API backend and a React/Typescript frontend

Background

Expertiza utilizes a system for instructors to define topics within assignments, allowing students (often in teams) to sign up for these topics. The previous implementation, centered around the `SignUpTopic` model and associated controllers (`SignUpTopicsController`, `SignedUpTeamsController`), lacked clarity in naming and had business logic distributed across controllers rather than encapsulated within models. Furthermore, the user interface for managing assignment topics by instructors and for students signing up for topics required modernization and improved functionality.

Motivation

This project aimed to improve the maintainability, clarity, and functionality of the topic management and signup features within Expertiza. Key motivations included:

  • Renaming core components (`SignUpTopic` -> `ProjectTopic`) to better reflect their purpose.
  • Centralizing business logic within models for better separation of concerns and testability.
  • Creating a more intuitive and feature-rich interface for instructors managing assignment topics.
  • Developing a dedicated and user-friendly interface for students to view, select, and manage their topic signups.
  • Introducing features like bookmarking topics and streamlining the student signup/drop process.
  • Updating dependencies to keep the application current.

Tasks Accomplished

  • Renamed `SignUpTopic` to `ProjectTopic` throughout the backend (models, controllers, routes, tests, database).
  • Refactored `ProjectTopic` and `SignedUpTeam` models to encapsulate business logic (topic creation/update/deletion, team signup/waitlisting, slot management, student signup/drop).
  • Removed `SignUpTopicsController` and implemented `ProjectTopicsController` using the refactored model logic.
  • Refactored `SignedUpTeamsController` to utilize new model methods and added new actions (`drop_topic`, `drop_team_from_topic`).
  • Added `allow_bookmarks` attribute to the `Assignment` model and associated backend/frontend logic.
  • Created a new frontend page (`AssignmentEditPage.tsx`) with a tabbed interface for instructors to manage assignments, including a dedicated "Topics" tab.
  • Implemented modals for creating, editing, importing, and deleting topics on the frontend.
  • Developed a reusable `TopicsTable.tsx` component for displaying topics in instructor and student views.
  • Created a new frontend page (`StudentTasks.tsx`) for students to view assignment topics, bookmark (if enabled), and sign up/drop topics.
  • Updated backend dependencies (Rails, Puma, Nokogiri, etc.) and Docker configuration.
  • Added/updated RSpec tests for `ProjectTopic` and `SignedUpTeam` models and updated relevant existing tests.
  • Updated Swagger API documentation.

Files

New Backend Files Created:

  • `app/controllers/project_topics_controller.rb`
  • `app/models/project_topic.rb`
  • `db/migrate/20250319015434_rename_sign_up_topics_to_project_topics.rb`
  • `db/migrate/20250321222753_rename_sign_up_topic_to_project_topic_in_signed_up_teams.rb`
  • `db/migrate/20251028200538_add_allow_bookmarks_to_assignments.rb`
  • `spec/factories/project_topics.rb`
  • `spec/models/project_topic_spec.rb`
  • `spec/models/signed_up_team_spec.rb`
  • `spec/routing/project_topics_routing_spec.rb`

Modified Backend Files:

  • `.ruby-version`, `Gemfile`, `Gemfile.lock`
  • `app/controllers/assignments_controller.rb`
  • `app/controllers/signed_up_teams_controller.rb`
  • `app/models/assignment.rb`
  • `app/models/bookmark.rb`
  • `app/models/signed_up_team.rb`
  • `app/models/user.rb`
  • `config/routes.rb`
  • `db/migrate/20231129050431_create_sign_up_topics.rb` (Renamed table)
  • `db/schema.rb`
  • `db/seeds.rb`
  • `docker-compose.yml`
  • `spec/factories/assignments.rb`
  • `spec/models/due_date_spec.rb`
  • `spec/rails_helper.rb`
  • `swagger/v1/swagger.yaml`

Deleted Backend Files:

  • `app/controllers/sign_up_topics_controller.rb`
  • `app/models/sign_up_topic.rb`
  • `spec/routing/sign_up_topics_routing_spec.rb`

New Frontend Files Created:

  • `public/assets/icons/Check-icon.png`
  • `src/pages/Assignments/AssignmentEditPage.tsx`
  • `src/pages/Assignments/components/TopicsTable.tsx`
  • `src/pages/Assignments/tabs/DueDatesTab.tsx`
  • `src/pages/Assignments/tabs/EtcTab.tsx`
  • `src/pages/Assignments/tabs/GeneralTab.tsx` (Note: This might be redundant if general editing stays in AssignmentEditor)
  • `src/pages/Assignments/tabs/ReviewStrategyTab.tsx`
  • `src/pages/Assignments/tabs/RubricsTab.tsx`
  • `src/pages/Assignments/tabs/TopicsTab.tsx`
  • `src/pages/StudentTasks/StudentTasks.tsx`

Modified Frontend Files:

  • `src/App.tsx` (Routing changes)
  • `src/components/Table/Table.tsx` (Added selected row styling)
  • `src/hooks/useAPI.ts` (Minor adjustment to isLoading)
  • `src/layout/Header.tsx` (Conditional assignments link for students)
  • `src/pages/Assignments/Assignment.tsx` (Adjusted to accommodate new edit page link)
  • `src/pages/Assignments/AssignmentEditor.tsx` (Potentially simplified if general settings move to GeneralTab)

Problem Analysis

Issues with Previous Implementation

Naming Convention

The name `SignUpTopic` was ambiguous. Renaming to `ProjectTopic` clarifies that these are topics related to the core project/assignment work students sign up for.

Distributed Business Logic

Logic for creating topics, signing up teams, managing waitlists, and checking slots was spread across `SignUpTopicsController` and `SignedUpTeamsController`. This violated the principle of "fat model, skinny controller", making the code harder to understand, test, and maintain.

Inefficient Data Fetching

Frontend components often needed to make multiple API calls or perform calculations (like available slots) that could be handled more efficiently on the backend. The previous `SignUpTopic` API didn't provide computed data like assigned/waitlisted teams directly.

Lack of Dedicated Student View

Students lacked a clear, dedicated interface to view available topics for an assignment and manage their selection. The signup process was not streamlined.

Inconsistent Assignment Editing

The existing `AssignmentEditor` modal combined general settings with links to other management pages (like topics, participants) in an ad-hoc manner. A more structured approach was needed.

Implementation

= Architecture Decisions

  • Model-Centric Logic: Shifted primary business logic for topic management and team signups into the `ProjectTopic` and `SignedUpTeam` models, respectively. Controllers now primarily handle request/response flow and delegate actions to the models.
  • Dedicated Edit Page (Frontend): Created a new full page (`AssignmentEditPage`) for editing assignments instead of using a modal, allowing for a more complex, tabbed interface.
  • Reusable Table Component (Frontend): Developed `TopicsTable` to handle the display and core interactions for topics in different contexts (instructor vs. student), promoting DRY principles.
  • API Enhancements: Modified the `ProjectTopic` index endpoint (`/api/v1/project_topics`) to accept `assignment_id` and return topics with computed data (`to_json_with_computed_data`), reducing frontend calculation needs. Added specific endpoints for student signup (`sign_up_student`) and dropping topics (`drop_topic`, `drop_team_from_topic`).

Core Functionality Implemented

Feature Backend Changes Frontend Changes
Topic Renaming Renamed `SignUpTopic` model, controller, routes, DB table to `ProjectTopic`. Updated associations. Updated API endpoints used.
Topic Management (Instructor) `ProjectTopicsController` CRUD actions delegating to `ProjectTopic` model methods (`create_topic_with_assignment`, `update_topic`, `delete_by_assignment_and_topic_ids`). API returns computed data. New `AssignmentEditPage` with `TopicsTab`. Uses `TopicsTable` (instructor mode). Modals for create, edit, import, delete. API calls for CRUD.
Team Signup Logic `ProjectTopic` methods (`sign_team_up`, `drop_team`, `promote_waitlisted_team`). `SignedUpTeam` methods (`create_signed_up_team`, `remove_team_signups`). Instructor view shows assigned/waitlisted teams in `TopicsTable` details. Drop team button calls backend.
Student Signup/Drop `SignedUpTeamsController` actions (`sign_up_student`, `drop_topic`) delegating to `SignedUpTeam` model methods (`sign_up_student_for_topic`, `drop_topic_for_student`). Automatic handling of switching topics. New `StudentTasks` page. Uses `TopicsTable` (student mode). Select/deselect buttons trigger API calls. Optimistic UI updates for slots. Displays current selection.
Topic Bookmarking Added `allow_bookmarks` boolean to `Assignment`. Added `project_topic_params` permit `allow_bookmarks` in `AssignmentsController`. `AssignmentEditPage` allows instructors to toggle `allow_bookmarks` (persisted via PATCH). `StudentTasks` shows bookmark toggle if `allow_bookmarks` is true (local state for now).
Assignment Editing UI N/A New `AssignmentEditPage` with tabbed structure. `AssignmentEditor` might be simplified or removed for general settings.
Dependency Updates Updated Ruby, Rails, Puma, Nokogiri etc. Added required gems. N/A

Key Backend Improvements

ProjectTopic Model

# Handles team signup, considering available slots and waitlisting
# Renamed from signup_team for clarity
def sign_team_up(team)
  return false if signed_up_teams.exists?(team: team)
  ActiveRecord::Base.transaction do
    signed_up_team = signed_up_teams.create!(
      team: team,
      is_waitlisted: !slot_available?
    )
    # Remove team from other waitlists if confirmed here
    remove_from_waitlist(team) unless signed_up_team.is_waitlisted?
    true
  end
rescue ActiveRecord::RecordInvalid
  false
end

# Handles dropping a team and promoting waitlisted if necessary
def drop_team(team)
  signed_up_team = signed_up_teams.find_by(team: team)
  return unless signed_up_team
  team_confirmed = !signed_up_team.is_waitlisted?
  signed_up_team.destroy!
  promote_waitlisted_team if team_confirmed # Promote only if a confirmed slot opened
end

# Calculates available slots
def available_slots
  max_choosers - confirmed_teams_count
end

# Checks if slots are positive
def slot_available?
  available_slots.positive?
end

# Business logic for creation, moved from controller
def self.create_topic_with_assignment(topic_params, assignment_id, micropayment = nil)
  # ... (find assignment, build topic, save, return hash { success:, message:, topic:/errors: })
end

# Business logic for update, moved from controller
def update_topic(topic_params)
  # ... (update attributes, return hash { success:, message:, topic:/errors: })
end

# Returns enhanced JSON including computed fields
def to_json_with_computed_data
  as_json.merge(
    available_slots: available_slots,
    confirmed_teams: confirmed_teams.map { |team| # ... format team data },
    waitlisted_teams: waitlisted_teams.map { |team| # ... format team data }
  )
end

private

# Helper for promotion logic
def promote_waitlisted_team
  # ... (find earliest waitlisted team, update is_waitlisted to false, remove from other waitlists)
end

# Helper to remove team from other topics' waitlists
def remove_from_waitlist(team)
  team.signed_up_teams.waitlisted.where.not(project_topic_id: id).destroy_all
end

SignedUpTeam Model

# Scope for confirmed signups
scope :confirmed, -> { where(is_waitlisted: false) }
# Scope for waitlisted signups
scope :waitlisted, -> { where(is_waitlisted: true) }

belongs_to :project_topic # Updated association
# ... validations ...

# Business logic for student signup, including dropping previous topic
def self.sign_up_student_for_topic(user_id, topic_id)
  team_id = get_team_participants(user_id) # Find student's team
  return { success: false, message: "User is not part of any team" } unless team_id

  # Drop any existing topic signups for this team FIRST
  drop_existing_signups_for_team(team_id)

  # Sign up for the new topic using create_signed_up_team (which calls topic.sign_team_up)
  signed_up_team = create_signed_up_team(topic_id, team_id)

  if signed_up_team
    { success: true, message: "Signed up team successful!", signed_up_team: signed_up_team, available_slots: signed_up_team.project_topic.available_slots }
  else
    { success: false, message: "Failed to sign up for topic. Topic may be full or already signed up." }
  end
end

# Business logic for dropping a topic
def self.drop_topic_for_student(user_id, topic_id)
  team_id = get_team_participants(user_id)
  # ... (find topic, team, signed_up_team record) ...
  return { success: false, message: "Team is not signed up for this topic" } unless signed_up_team

  # Delegate drop logic to ProjectTopic
  project_topic.drop_team(team)

  { success: true, message: "Successfully dropped topic!", available_slots: project_topic.available_slots }
end

# Helper to find and drop existing signups before signing up for a new one
private
def self.drop_existing_signups_for_team(team_id)
  existing_signups = where(team_id: team_id)
  existing_signups.each do |signup|
    signup.project_topic.drop_team(signup.team) # Use ProjectTopic's drop method
  end
end

Frontend Improvements

AssignmentEditPage Component

// Manages overall state for assignment editing
const AssignmentEditPage = () => {
  // ... hooks for fetching data (assignment, topics), state for active tab, topic settings, topics data ...

  // Fetch assignment details on load
  useEffect(() => {
    if (id) fetchAssignment({ url: `/assignments/${id}` });
  }, [id, fetchAssignment]);

  // Update local state when assignment data is loaded (e.g., name, allow_bookmarks)
  useEffect(() => {
    if (assignmentResponse?.data) {
      setAssignmentName(assignmentResponse.data.name || "");
      setTopicSettings(prev => ({ ...prev, allowBookmarks: assignmentResponse.data.allow_bookmarks }));
    }
  }, [assignmentResponse]);

  // Fetch topics when assignment ID is known
  useEffect(() => {
    if (id) fetchTopics({ url: `/project_topics?assignment_id=${id}` });
  }, [id, fetchTopics]);

  // Process fetched topics into format needed by TopicsTable
  useEffect(() => {
     if (topicsResponse?.data) {
       // ... transform backend topic structure to frontend TopicData structure ...
       setTopicsData(transformedTopics);
     }
  }, [topicsResponse]);

  // Handler for topic setting changes (e.g., checkboxes)
  const handleTopicSettingChange = useCallback((setting: string, value: boolean) => {
    setTopicSettings((prev) => ({ ...prev, [setting]: value }));
    // Persist allowBookmarks change immediately
    if (setting === 'allowBookmarks' && id) {
      updateAssignment({ /* ... PATCH request data ... */ });
    }
  }, [id, updateAssignment]);

  // Handlers for topic CRUD operations (passed down to TopicsTab)
  const handleDeleteTopic = useCallback((topicId: string) => {
    // ... call deleteTopic API ...
  }, [id, deleteTopic]);
  // ... other handlers (create, edit, drop team) ...

  // Renders the content based on the active tab state
  const renderTabContent = () => {
    switch (activeTab) {
      case "topics":
        return <TopicsTab /* pass props: settings, data, handlers */ />;
      // ... other cases for RubricsTab, DueDatesTab etc. ...
    }
  };

  return (
    // ... JSX for layout, tab buttons, calling renderTabContent ...
  );
};

StudentTasks Component

// Page for students to sign up for topics
const StudentTasks: React.FC = () => {
  // ... hooks for fetching (assignments, topics), signing up, dropping ...
  // ... state for bookmarks, selectedTopic, loading states, optimistic updates ...

  // Fetch assignment and then topics
  useEffect(() => { /* ... fetch assignment data ... */ }, [/* dependencies */]);
  useEffect(() => { /* ... fetch topics based on assignment id ... */ }, [/* dependencies */]);

  // Process topics data, applying optimistic updates
  const topics = useMemo(() => {
    // ... map topicsResponse.data, check optimisticSlotChanges map ...
  }, [/* dependencies */]);

  // Check if bookmarks allowed
  const allowBookmarks = useMemo(() => {
     // ... check assignmentResponse.data.allow_bookmarks ...
  }, [assignmentResponse]);

  // Handle selecting/deselecting a topic
  const handleTopicSelect = useCallback(async (topicId: string) => {
    if (selectedTopic === topicId) { // Deselecting
      // Optimistically update slots (+1)
      // Set selectedTopic to null
      // Call dropAPI
    } else { // Selecting a new topic
      // Optimistically update slots (-1 for new, +1 for old if exists)
      // Call dropAPI for old topic if selectedTopic is not null
      // Set selectedTopic to topicId
      // Call signUpAPI
    }
  }, [/* dependencies: selectedTopic, currentUser, dropAPI, signUpAPI, topics */]);

  // Handle bookmark toggle (local state)
  const handleBookmarkToggle = useCallback((topicId: string) => { /* ... update bookmarkedTopics set ... */ }, []);

  // Prepare data for TopicsTable
  const topicRows: TopicRow[] = useMemo(() => topics.map(t => ({ /* ... map topic data ... */ })), [topics]);

  // ... return JSX ...
  return (
    <Container>
      {/* ... Title, Current Selection display ... */}
      {topicsLoading ? <Spinner /> : topicsError ? <Alert variant="danger">{topicsError}</Alert> : (
        <TopicsTable
          data={topicRows}
          mode="student"
          onBookmarkToggle={handleBookmarkToggle}
          onSelectTopic={handleTopicSelect}
          isSigningUp={isSigningUp}
          selectedTopicId={selectedTopic}
          showBookmarks={allowBookmarks}
          // ... other props ...
        />
      )}
    </Container>
  );
};

Database Migrations

  • Renamed `sign_up_topics` table to `project_topics`.
  • Updated `signed_up_teams` table: renamed `sign_up_topic_id` foreign key to `project_topic_id` and updated index name.
  • Added `allow_bookmarks` boolean column to `assignments` table.
  • Renamed `teams_users` join table to `teams_participants`.

Testing

RSpec Tests (Backend)

  • ProjectTopic Model (`project_topic_spec.rb`): Added extensive tests covering:
   * `sign_team_up`: Confirmed vs. waitlisted based on `max_choosers`, removal from other waitlists.
   * `drop_team`: Team removal, promotion of earliest waitlisted team.
   * `available_slots`, `slot_available?`: Correct calculations before/after signups/drops.
   * `confirmed_teams`, `waitlisted_teams`: Correct retrieval and ordering.
   * Validations: Presence of `topic_name`, non-negative `max_choosers` (including zero).
  • SignedUpTeam Model (`signed_up_team_spec.rb`): Added tests covering:
   * Validations: Presence of topic/team, uniqueness scope.
   * Scopes: `:confirmed`, `:waitlisted`.
   * Class methods: Signup logic delegation, removal logic, participant/topic finding.
   * Waitlisting behavior when topic is full.
  • Updates: Modified existing tests (e.g., `due_date_spec.rb`) to reference `ProjectTopic`.

Manual Tests (UI)

  • Instructor Flow: Verified assignment editing via `/assignments/edit/:id`, topic creation/editing/deletion/import via modals, toggling `allow_bookmarks`, dropping teams from topics.
  • Student Flow: Verified topic viewing via `/student_tasks/:assignmentId`, selecting/deselecting topics (including automatic drop of previous), bookmarking (when enabled), correct display of slots and waitlists, disabled selection for full topics.

Impact Analysis

Summary of Changes

  • Backend: Major refactoring of topic-related models and controllers, database schema changes, new API endpoints, updated dependencies. Approximately [Count Lines] lines added/modified across [Count Files] files (primarily models, controllers, specs, migrations).
  • Frontend: New assignment editing page with tabbed UI, new student task page for topic signup, reusable `TopicsTable` component, new routes. Approximately [Count Lines] lines added/modified across [Count Files] files (primarily in `src/pages/Assignments`, `src/pages/StudentTasks`, `src/components`).

Achievements

  • ✓ Successfully refactored `SignUpTopic` to `ProjectTopic` for improved clarity.
  • ✓ Centralized business logic in models, resulting in cleaner controllers.
  • ✓ Implemented a modern, tabbed interface for assignment editing.
  • ✓ Created a dedicated, streamlined UI for student topic signup.
  • ✓ Added topic bookmarking functionality.
  • ✓ Enhanced API endpoints for better data provision to the frontend.
  • ✓ Updated key dependencies (Rails 8).
  • ✓ Added comprehensive RSpec tests for new model logic.

Future Work

  • Frontend Bookmark Persistence: Implement API calls to save/load student topic bookmarks instead of using local state.
  • Partner Ad Feature: Fully implement the "Apply to partner ad" functionality hinted at in `TopicsTab`.
  • Complete Assignment Tabs: Implement the remaining tabs in `AssignmentEditPage` (Rubrics, Review Strategy, Due Dates, Etc.).
  • Refine General Tab/AssignmentEditor: Decide whether general assignment settings remain in `AssignmentEditor` modal or move entirely to a "General" tab within `AssignmentEditPage`.
  • Error Handling (Frontend): Enhance user feedback for API errors during topic operations (create/edit/delete/signup/drop).
  • Authorization Checks: Ensure robust authorization checks are in place for all new/modified backend endpoints.

Conclusion

This project successfully refactored the core topic management and signup system in Expertiza, replacing `SignUpTopic` with `ProjectTopic` and significantly improving the underlying logic by moving it into the models. New frontend interfaces provide a much-improved experience for both instructors managing topics and students signing up for them. The addition of features like topic bookmarking and a streamlined student signup flow enhances usability. The changes establish a more maintainable and understandable codebase for future development.

References

  1. Expertiza on GitHub
  2. The live Expertiza website
  3. Expertiza project documentation wiki
  4. (Add links to your specific project fork/PRs if needed)
  5. Ruby on Rails Guides
  6. React Documentation
  7. Rspec Documentation

Code Repositories

Backend PR: (Insert Link to Backend PR) Frontend PR: (Insert Link to Frontend PR)