From 0e8a31407ef387cf8b3e82c5b350a8546a84650a Mon Sep 17 00:00:00 2001 From: cjbhatna Date: Thu, 26 Mar 2026 20:07:57 -0400 Subject: [PATCH 01/10] Set up view submissions API call --- app/controllers/assignments_controller.rb | 32 +++++++++++++++++++++++ config/routes.rb | 1 + db/schema.rb | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 95cd55220..1a0c8693d 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -210,6 +210,38 @@ def varying_rubrics_by_round? end end + # GET /assignments/:id/view_submissions + def view_submissions + assignment = Assignment.find_by(id: params[:id]) + return render json: { error: "Assignment not found" }, status: :not_found if assignment.nil? + + submissions = assignment.teams.map do |team| + members = team.teams_users.includes(:user).map do |tu| + user = tu.user + { + full_name: user&.full_name, + github: "", + email: user&.email + } + end + + { + id: team.id, + team_id: team.id, + team_name: team.name, + members: members, + links: [], + files: [] + } + end + + render json: { + assignment_id: assignment.id, + assignment_name: assignment.name, + submissions: submissions + }, status: :ok + end + private # Only allow a list of trusted parameters through. def assignment_params diff --git a/config/routes.rb b/config/routes.rb index 57559d007..9b9936dcc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,6 +37,7 @@ get '/:assignment_id/valid_num_review/:review_type', action: :valid_num_review get '/:assignment_id/varying_rubrics_by_round', action: :varying_rubrics_by_round? post '/:assignment_id/create_node',action: :create_node + get '/:id/view_submissions', action: :view_submissions end end diff --git a/db/schema.rb b/db/schema.rb index cddbe12c6..6a198491c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -456,9 +456,9 @@ add_foreign_key "assignments_duties", "duties" add_foreign_key "courses", "institutions" add_foreign_key "courses", "users", column: "instructor_id" + add_foreign_key "duties", "users", column: "instructor_id" add_foreign_key "invitations", "participants", column: "from_id" add_foreign_key "invitations", "participants", column: "to_id" - add_foreign_key "duties", "users", column: "instructor_id" add_foreign_key "items", "questionnaires" add_foreign_key "participants", "duties" add_foreign_key "participants", "join_team_requests" From 9714ef86ad365fe28c7546db2b0792566f4777bf Mon Sep 17 00:00:00 2001 From: CamBhat Date: Tue, 21 Apr 2026 13:30:41 -0400 Subject: [PATCH 02/10] Add view_submissions action to controller Introduce GET /submitted_content/:id/view_submissions in SubmittedContentController. The action finds the assignment by id (returns 404 if missing), builds a submissions array grouped by team (includes team id, name, members with full_name and email, and placeholder github/links/files), and returns assignment metadata with submissions in JSON (200 OK). Uses teams_users.includes(:user) to preload users. --- .../submitted_content_controller.rb | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/app/controllers/submitted_content_controller.rb b/app/controllers/submitted_content_controller.rb index a84dd6812..f82116aa9 100644 --- a/app/controllers/submitted_content_controller.rb +++ b/app/controllers/submitted_content_controller.rb @@ -302,6 +302,39 @@ def list_files render_error("Failed to list directory contents: #{e.message}. Please try again.", :internal_server_error) end + # GET /submitted_content/:id/view_submissions + # Retrieves submission data for a given assignment, grouped by team + def view_submissions + assignment = Assignment.find_by(id: params[:id]) + return render json: { error: "Assignment not found" }, status: :not_found if assignment.nil? + + submissions = assignment.teams.map do |team| + members = team.teams_users.includes(:user).map do |tu| + user = tu.user + { + full_name: user&.full_name, + github: "", + email: user&.email + } + end + + { + id: team.id, + team_id: team.id, + team_name: team.name, + members: members, + links: [], + files: [] + } + end + + render json: { + assignment_id: assignment.id, + assignment_name: assignment.name, + submissions: submissions + }, status: :ok + end + private # Before action callback: Sets @submission_record for the show action From e35a29e379b9ea4942f564e7c7e9baadd1a3648d Mon Sep 17 00:00:00 2001 From: CamBhat Date: Tue, 21 Apr 2026 13:31:27 -0400 Subject: [PATCH 03/10] Remove view_submissions action Remove the AssignmentsController#view_submissions action (GET /assignments/:id/view_submissions). The deleted method built a submissions payload by enumerating assignment.teams and their members (including full_name and email, with empty github/links/files). This cleans up the controller by removing the now-unnecessary endpoint and its response construction. --- app/controllers/assignments_controller.rb | 32 ----------------------- 1 file changed, 32 deletions(-) diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 1a0c8693d..e16ec85fa 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -209,38 +209,6 @@ def varying_rubrics_by_round? end end end - - # GET /assignments/:id/view_submissions - def view_submissions - assignment = Assignment.find_by(id: params[:id]) - return render json: { error: "Assignment not found" }, status: :not_found if assignment.nil? - - submissions = assignment.teams.map do |team| - members = team.teams_users.includes(:user).map do |tu| - user = tu.user - { - full_name: user&.full_name, - github: "", - email: user&.email - } - end - - { - id: team.id, - team_id: team.id, - team_name: team.name, - members: members, - links: [], - files: [] - } - end - - render json: { - assignment_id: assignment.id, - assignment_name: assignment.name, - submissions: submissions - }, status: :ok - end private # Only allow a list of trusted parameters through. From 0465dced071c23b6be754c934986b019c2d45cf5 Mon Sep 17 00:00:00 2001 From: CamBhat Date: Tue, 21 Apr 2026 13:33:22 -0400 Subject: [PATCH 04/10] Add submitted_content routes and view_submissions member Define additional collection routes for submitted_content (download, list_files, remove_hyperlink, submit_file, submit_hyperlink, folder_action) and move the existing view_submissions route into a member block. This restructures the routes to properly scope actions under the submitted_content resource and removes the previous standalone get route. --- config/routes.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 9b9936dcc..160bae62d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,7 +37,22 @@ get '/:assignment_id/valid_num_review/:review_type', action: :valid_num_review get '/:assignment_id/varying_rubrics_by_round', action: :varying_rubrics_by_round? post '/:assignment_id/create_node',action: :create_node - get '/:id/view_submissions', action: :view_submissions + end + end + + resources :submitted_content do + collection do + get :download + get :list_files + delete :remove_hyperlink + post :submit_file + post :submit_hyperlink + post :folder_action + end + + # ADD THIS: + member do + get :view_submissions end end From 74c82ede27530ecdd5d49d3184d6a2fb617c3c87 Mon Sep 17 00:00:00 2001 From: CamBhat Date: Tue, 21 Apr 2026 15:42:40 -0400 Subject: [PATCH 05/10] Remove submitted_content routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete the commented placeholder and the resources :submitted_content block from config/routes.rb. This removes the submitted_content collection routes (download, list_files, remove_hyperlink, submit_file, submit_hyperlink, folder_action), cleaning up the routing file—these endpoints appear deprecated or have been moved elsewhere. --- config/routes.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 160bae62d..c2c77ef24 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,7 +50,6 @@ post :folder_action end - # ADD THIS: member do get :view_submissions end @@ -124,17 +123,6 @@ delete :remove_advertisement end end - - resources :submitted_content do - collection do - get :download - get :list_files - delete :remove_hyperlink - post :submit_file - post :submit_hyperlink - post :folder_action - end - end resources :join_team_requests do member do From 35fc36a4229e195828dccd6ed660f42b4cdd8f1e Mon Sep 17 00:00:00 2001 From: CamBhat Date: Tue, 21 Apr 2026 15:55:34 -0400 Subject: [PATCH 06/10] Load SubmissionRecords into view_submissions Populate the view_submissions response with actual SubmissionRecord data instead of empty arrays. The controller now queries SubmissionRecord for a team+assignment, maps 'hyperlink' records to link objects (id, url, display_name, name, type='Hyperlink', modified) and 'file' records to file objects (id, basename as display_name/name, ext as type uppercased, modified). Add seed data to create sample hyperlink and file SubmissionRecord entries per team: sample URLs and file metadata are used to create records (and call team.submit_hyperlink where appropriate) with randomized created_at timestamps to help testing and demos. --- .../submitted_content_controller.rb | 30 +++++++++-- db/seeds.rb | 54 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/app/controllers/submitted_content_controller.rb b/app/controllers/submitted_content_controller.rb index f82116aa9..5f73d9718 100644 --- a/app/controllers/submitted_content_controller.rb +++ b/app/controllers/submitted_content_controller.rb @@ -303,7 +303,6 @@ def list_files end # GET /submitted_content/:id/view_submissions - # Retrieves submission data for a given assignment, grouped by team def view_submissions assignment = Assignment.find_by(id: params[:id]) return render json: { error: "Assignment not found" }, status: :not_found if assignment.nil? @@ -318,13 +317,38 @@ def view_submissions } end + # Pull SubmissionRecords for this team + records = SubmissionRecord.where(team_id: team.id, assignment_id: assignment.id) + + links = records.where(record_type: 'hyperlink').map do |r| + { + id: r.id, + url: r.content, + display_name: r.content, + name: r.content, + type: 'Hyperlink', + modified: r.created_at + } + end + + files = records.where(record_type: 'file').map do |r| + { + id: r.id, + url: nil, + display_name: File.basename(r.content), + name: File.basename(r.content), + type: File.extname(r.content).delete('.').upcase, + modified: r.created_at + } + end + { id: team.id, team_id: team.id, team_name: team.name, members: members, - links: [], - files: [] + links: links, + files: files } end diff --git a/db/seeds.rb b/db/seeds.rb index d49c80f33..2aabd774c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -135,6 +135,60 @@ end end + puts "creating submission records (hyperlinks and files)" + hyperlink_samples = [ + "https://github.com/ncsu-csc/project-repo", + "https://github.com/ncsu-csc/demo-app", + "https://youtu.be/dQw4w9WgXcQ", + "https://docs.google.com/presentation/d/example", + "https://github.com/student-team/final-project" + ] + + file_samples = [ + { name: "report.pdf", size: 204800, type: "pdf" }, + { name: "slides.pptx", size: 1048576, type: "pptx" }, + { name: "design-doc.docx", size: 512000, type: "docx" }, + { name: "demo.mp4", size: 5242880, type: "mp4" }, + { name: "readme.md", size: 4096, type: "md" } + ] + + num_teams.times do |i| + team = AssignmentTeam.find(team_ids[i]) + participant = AssignmentParticipant.find_by(team_id: team.id) + next unless participant + + assignment_id = team.parent_id + + # Add 1-2 hyperlinks per team + rand(1..2).times do |j| + url = hyperlink_samples[(i + j) % hyperlink_samples.length] + # Store on the team's hyperlinks list + team.submit_hyperlink(url) rescue nil + # Create audit record + SubmissionRecord.create( + record_type: 'hyperlink', + content: url, + operation: 'Submit Hyperlink', + team_id: team.id, + user: participant.user&.name || "student#{i}", + assignment_id: assignment_id, + created_at: Faker::Time.backward(days: 14) + ) + end + + # Add 1 file record per team + file = file_samples[i % file_samples.length] + SubmissionRecord.create( + record_type: 'file', + content: "/submissions/#{assignment_id}/#{team.id}/#{file[:name]}", + operation: 'Submit File', + team_id: team.id, + user: participant.user&.name || "student#{i}", + assignment_id: assignment_id, + created_at: Faker::Time.backward(days: 7) + ) + end + rescue ActiveRecord::RecordInvalid => e puts e, 'The db has already been seeded' end From 65f045d3c699c0385694857a3782ccf82921bcb0 Mon Sep 17 00:00:00 2001 From: CamBhat Date: Wed, 22 Apr 2026 08:52:19 -0400 Subject: [PATCH 07/10] Support external file URLs and update seeds Use external URLs for file records and generate internal download URLs only for local files. In SubmittedContentController, detect content starting with 'http' and expose it as the file URL; otherwise build an encoded internal download path. Update db/seeds.rb to use external sample file URLs (instead of local submission paths), store those URLs in SubmissionRecord.content, and fix assignment_id to use the team's parent_id. These changes allow seeding with real remote sample files and ensure correct assignment linkage. --- app/controllers/submitted_content_controller.rb | 3 ++- db/seeds.rb | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/controllers/submitted_content_controller.rb b/app/controllers/submitted_content_controller.rb index 5f73d9718..a2782ebec 100644 --- a/app/controllers/submitted_content_controller.rb +++ b/app/controllers/submitted_content_controller.rb @@ -332,9 +332,10 @@ def view_submissions end files = records.where(record_type: 'file').map do |r| + is_external = r.content.to_s.start_with?('http') { id: r.id, - url: nil, + url: is_external ? r.content : "/submitted_content/download?download=#{URI.encode_uri_component(filename)}¤t_folder[name]=/", display_name: File.basename(r.content), name: File.basename(r.content), type: File.extname(r.content).delete('.').upcase, diff --git a/db/seeds.rb b/db/seeds.rb index 2aabd774c..7ad755524 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -145,11 +145,9 @@ ] file_samples = [ - { name: "report.pdf", size: 204800, type: "pdf" }, - { name: "slides.pptx", size: 1048576, type: "pptx" }, - { name: "design-doc.docx", size: 512000, type: "docx" }, - { name: "demo.mp4", size: 5242880, type: "mp4" }, - { name: "readme.md", size: 4096, type: "md" } + { name: "report.pdf", url: "https://www.w3.org/WAI/UR/terms/media/sample.pdf", type: "pdf" }, + { name: "slides.pptx", url: "https://file-examples.com/storage/sample.pptx", type: "pptx" }, + { name: "readme.md", url: "https://raw.githubusercontent.com/github/docs/main/README.md", type: "md" } ] num_teams.times do |i| @@ -176,15 +174,14 @@ ) end - # Add 1 file record per team file = file_samples[i % file_samples.length] SubmissionRecord.create( record_type: 'file', - content: "/submissions/#{assignment_id}/#{team.id}/#{file[:name]}", + content: file[:url], # store real URL instead of local path operation: 'Submit File', team_id: team.id, user: participant.user&.name || "student#{i}", - assignment_id: assignment_id, + assignment_id: team.parent_id, created_at: Faker::Time.backward(days: 7) ) end From 4826d67f5bab91f66c4a70d7c7983ce969db2394 Mon Sep 17 00:00:00 2001 From: CamBhat Date: Wed, 22 Apr 2026 09:00:40 -0400 Subject: [PATCH 08/10] Add request specs for view_submissions endpoint Add a comprehensive RSpec request spec (spec/requests/view_submissions_spec.rb) for GET /submitted_content/:id/view_submissions. The new tests cover: assignment-not-found, assignments with no teams, teams with/without submission records, hyperlink and file submissions (including type/name extraction), filtering by assignment, multiple teams, teams with no members, and response shape contract. Test setup creates institutions, users, teams, and SubmissionRecord fixtures and uses JsonWebToken for auth. --- spec/requests/view_submissions_spec.rb | 393 +++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 spec/requests/view_submissions_spec.rb diff --git a/spec/requests/view_submissions_spec.rb b/spec/requests/view_submissions_spec.rb new file mode 100644 index 000000000..3e46ff24e --- /dev/null +++ b/spec/requests/view_submissions_spec.rb @@ -0,0 +1,393 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'json_web_token' + +RSpec.describe 'GET /submitted_content/:id/view_submissions', type: :request do + # ------------------------------------------------------------------ + # Shared test data + # ------------------------------------------------------------------ + let(:institution) { Institution.create!(name: 'NC State University') } + + let(:instructor) do + User.create!( + name: 'instructor1', + email: 'instructor@example.com', + password: 'password', + full_name: 'Test Instructor', + institution_id: institution.id, + role_id: Role.find_or_create_by!(name: 'Instructor').id + ) + end + + let(:assignment) do + Assignment.create!( + name: 'Test Assignment', + instructor_id: instructor.id, + has_teams: true, + private: false + ) + end + + let(:student_role) { Role.find_or_create_by!(name: 'Student') } + + let(:student1) do + User.create!( + name: 'student1', + email: 'student1@example.com', + password: 'password', + full_name: 'Alice Smith', + institution_id: institution.id, + role_id: student_role.id + ) + end + + let(:student2) do + User.create!( + name: 'student2', + email: 'student2@example.com', + password: 'password', + full_name: 'Bob Jones', + institution_id: institution.id, + role_id: student_role.id + ) + end + + let(:team) do + AssignmentTeam.create!(name: 'Team Alpha', parent_id: assignment.id) + end + + let!(:teams_user1) { TeamsUser.create!(team_id: team.id, user_id: student1.id) } + let!(:teams_user2) { TeamsUser.create!(team_id: team.id, user_id: student2.id) } + + let(:auth_headers) do + token = JsonWebToken.encode({ id: instructor.id }) + { 'Authorization' => "Bearer #{token}" } + end + + # ------------------------------------------------------------------ + # Helper + # ------------------------------------------------------------------ + def get_view_submissions(id, headers: auth_headers) + get "/submitted_content/#{id}/view_submissions", headers: headers + end + + # ================================================================== + # 1. Assignment not found + # ================================================================== + describe 'when assignment does not exist' do + it 'returns 404 with an error message' do + get_view_submissions(999_999) + + expect(response).to have_http_status(:not_found) + expect(response.parsed_body['error']).to eq('Assignment not found') + end + end + + # ================================================================== + # 2. Assignment exists but has no teams + # ================================================================== + describe 'when assignment has no teams' do + let(:empty_assignment) do + Assignment.create!( + name: 'Empty Assignment', + instructor_id: instructor.id, + has_teams: true, + private: false + ) + end + + it 'returns 200 with an empty submissions array' do + get_view_submissions(empty_assignment.id) + + expect(response).to have_http_status(:ok) + body = response.parsed_body + expect(body['assignment_id']).to eq(empty_assignment.id) + expect(body['submissions']).to eq([]) + end + end + + # ================================================================== + # 3. Assignment with teams but no submission records + # ================================================================== + describe 'when teams exist but have no submission records' do + it 'returns 200 with teams listed but empty links and files' do + get_view_submissions(assignment.id) + + expect(response).to have_http_status(:ok) + body = response.parsed_body + + expect(body['assignment_id']).to eq(assignment.id) + expect(body['assignment_name']).to eq('Test Assignment') + expect(body['submissions'].length).to eq(1) + + submission = body['submissions'].first + expect(submission['team_name']).to eq('Team Alpha') + expect(submission['links']).to eq([]) + expect(submission['files']).to eq([]) + end + + it 'includes all team members with correct fields' do + get_view_submissions(assignment.id) + + members = response.parsed_body['submissions'].first['members'] + expect(members.length).to eq(2) + + emails = members.map { |m| m['email'] } + expect(emails).to contain_exactly(student1.email, student2.email) + + full_names = members.map { |m| m['full_name'] } + expect(full_names).to contain_exactly(student1.full_name, student2.full_name) + end + end + + # ================================================================== + # 4. Team with hyperlink submission records + # ================================================================== + describe 'when team has hyperlink submission records' do + let!(:hyperlink_record) do + SubmissionRecord.create!( + record_type: 'hyperlink', + content: 'https://github.com/student1/project', + operation: 'Submit Hyperlink', + team_id: team.id, + user: student1.name, + assignment_id: assignment.id + ) + end + + it 'returns the hyperlink in the links array' do + get_view_submissions(assignment.id) + + expect(response).to have_http_status(:ok) + links = response.parsed_body['submissions'].first['links'] + expect(links.length).to eq(1) + expect(links.first['url']).to eq('https://github.com/student1/project') + expect(links.first['type']).to eq('Hyperlink') + end + + it 'does not include hyperlinks in the files array' do + get_view_submissions(assignment.id) + + files = response.parsed_body['submissions'].first['files'] + expect(files).to eq([]) + end + + it 'returns multiple hyperlinks when several exist' do + SubmissionRecord.create!( + record_type: 'hyperlink', + content: 'https://youtu.be/demo-video', + operation: 'Submit Hyperlink', + team_id: team.id, + user: student2.name, + assignment_id: assignment.id + ) + + get_view_submissions(assignment.id) + + links = response.parsed_body['submissions'].first['links'] + expect(links.length).to eq(2) + urls = links.map { |l| l['url'] } + expect(urls).to contain_exactly( + 'https://github.com/student1/project', + 'https://youtu.be/demo-video' + ) + end + end + + # ================================================================== + # 5. Team with file submission records + # ================================================================== + describe 'when team has file submission records' do + let!(:file_record) do + SubmissionRecord.create!( + record_type: 'file', + content: 'https://example.com/uploads/report.pdf', + operation: 'Submit File', + team_id: team.id, + user: student1.name, + assignment_id: assignment.id + ) + end + + it 'returns the file in the files array' do + get_view_submissions(assignment.id) + + expect(response).to have_http_status(:ok) + files = response.parsed_body['submissions'].first['files'] + expect(files.length).to eq(1) + expect(files.first['name']).to eq('report.pdf') + expect(files.first['url']).to eq('https://example.com/uploads/report.pdf') + end + + it 'correctly extracts file extension as type' do + get_view_submissions(assignment.id) + + files = response.parsed_body['submissions'].first['files'] + expect(files.first['type']).to eq('PDF') + end + + it 'does not include files in the links array' do + get_view_submissions(assignment.id) + + links = response.parsed_body['submissions'].first['links'] + expect(links).to eq([]) + end + end + + # ================================================================== + # 6. Team with both hyperlinks and files + # ================================================================== + describe 'when team has both hyperlinks and files' do + before do + SubmissionRecord.create!( + record_type: 'hyperlink', + content: 'https://github.com/team/repo', + operation: 'Submit Hyperlink', + team_id: team.id, + user: student1.name, + assignment_id: assignment.id + ) + SubmissionRecord.create!( + record_type: 'file', + content: 'https://example.com/slides.pptx', + operation: 'Submit File', + team_id: team.id, + user: student2.name, + assignment_id: assignment.id + ) + end + + it 'returns links and files in separate arrays' do + get_view_submissions(assignment.id) + + submission = response.parsed_body['submissions'].first + expect(submission['links'].length).to eq(1) + expect(submission['files'].length).to eq(1) + expect(submission['links'].first['type']).to eq('Hyperlink') + expect(submission['files'].first['type']).to eq('PPTX') + end + end + + # ================================================================== + # 7. Submission records from another assignment are not included + # ================================================================== + describe 'when submission records exist for a different assignment' do + let(:other_assignment) do + Assignment.create!( + name: 'Other Assignment', + instructor_id: instructor.id, + has_teams: true, + private: false + ) + end + + before do + SubmissionRecord.create!( + record_type: 'hyperlink', + content: 'https://github.com/other/repo', + operation: 'Submit Hyperlink', + team_id: team.id, + user: student1.name, + assignment_id: other_assignment.id # different assignment + ) + end + + it 'does not include records from other assignments' do + get_view_submissions(assignment.id) + + links = response.parsed_body['submissions'].first['links'] + expect(links).to eq([]) + end + end + + # ================================================================== + # 8. Multiple teams + # ================================================================== + describe 'when the assignment has multiple teams' do + let(:team2) { AssignmentTeam.create!(name: 'Team Beta', parent_id: assignment.id) } + let(:student3) do + User.create!( + name: 'student3', + email: 'student3@example.com', + password: 'password', + full_name: 'Carol White', + institution_id: institution.id, + role_id: student_role.id + ) + end + + before do + TeamsUser.create!(team_id: team2.id, user_id: student3.id) + SubmissionRecord.create!( + record_type: 'hyperlink', + content: 'https://github.com/team2/repo', + operation: 'Submit Hyperlink', + team_id: team2.id, + user: student3.name, + assignment_id: assignment.id + ) + end + + it 'returns a submission entry for each team' do + get_view_submissions(assignment.id) + + submissions = response.parsed_body['submissions'] + expect(submissions.length).to eq(2) + team_names = submissions.map { |s| s['team_name'] } + expect(team_names).to contain_exactly('Team Alpha', 'Team Beta') + end + + it 'only includes each team\'s own records' do + get_view_submissions(assignment.id) + + submissions = response.parsed_body['submissions'] + alpha = submissions.find { |s| s['team_name'] == 'Team Alpha' } + beta = submissions.find { |s| s['team_name'] == 'Team Beta' } + + expect(alpha['links']).to eq([]) + expect(beta['links'].first['url']).to eq('https://github.com/team2/repo') + end + end + + # ================================================================== + # 9. Team with no members + # ================================================================== + describe 'when a team has no members' do + let!(:empty_team) { AssignmentTeam.create!(name: 'Empty Team', parent_id: assignment.id) } + + it 'returns the team with an empty members array' do + get "/submitted_content/#{assignment.id}/view_submissions", headers: auth_headers + + submission = response.parsed_body['submissions'].find { |s| s['team_name'] == 'Empty Team' } + expect(submission).not_to be_nil + expect(submission['members']).to eq([]) + end + end + + # ================================================================== + # 10. Response shape contract + # ================================================================== + describe 'response shape' do + it 'always includes required top-level keys' do + get_view_submissions(assignment.id) + + body = response.parsed_body + expect(body.keys).to include('assignment_id', 'assignment_name', 'submissions') + end + + it 'each submission always includes required keys' do + get_view_submissions(assignment.id) + + submission = response.parsed_body['submissions'].first + expect(submission.keys).to include('id', 'team_id', 'team_name', 'members', 'links', 'files') + end + + it 'each member always includes required keys' do + get_view_submissions(assignment.id) + + member = response.parsed_body['submissions'].first['members'].first + expect(member.keys).to include('full_name', 'email', 'github') + end + end +end \ No newline at end of file From de88372224500c62ff82c74924434f520f484bb3 Mon Sep 17 00:00:00 2001 From: mcarthur-reece Date: Wed, 22 Apr 2026 17:28:18 -0400 Subject: [PATCH 09/10] fixed the submitted_content_helper.rb to allow deletion of submitted student files --- app/helpers/submitted_content_helper.rb | 46 +++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/app/helpers/submitted_content_helper.rb b/app/helpers/submitted_content_helper.rb index 2b6b19033..5ef599e8f 100644 --- a/app/helpers/submitted_content_helper.rb +++ b/app/helpers/submitted_content_helper.rb @@ -214,14 +214,17 @@ def copy_selected_file def delete_selected_files # Wrap the delete operation with error handling handle_file_operation_error('deleting') do + files_to_delete = resolved_files_for_deletion + + if files_to_delete.empty? + render json: { error: 'No files were selected for deletion.' }, status: :bad_request + return + end + # Track successfully deleted files for response deleted_files = [] - # Iterate through each file index in the chk_files param - Array(params[:chk_files]).each do |idx| - # Build the full file path for this index - file_path = File.join(params[:directories][idx], params[:filenames][idx]) - + files_to_delete.each do |file_path| # Check if file exists before attempting deletion if File.exist?(file_path) # Remove file or directory recursively @@ -231,7 +234,7 @@ def delete_selected_files deleted_files << file_path else # File doesn't exist, return error - render json: { error: "Cannot delete '#{params[:filenames][idx]}': File does not exist. It may have already been deleted." }, status: :not_found + render json: { error: "Cannot delete '#{File.basename(file_path)}': File does not exist. It may have already been deleted." }, status: :not_found return end end @@ -258,4 +261,35 @@ def create_new_folder render json: { message: "Directory '#{params[:faction][:create]}' created successfully." }, status: :created end end + + def resolved_files_for_deletion + current_folder_param = + params.dig(:current_folder, :name) || + params.dig('current_folder', 'name') || + params[:current_folder] || + params['current_folder'] + delete_target = params.dig(:faction, :delete) || params.dig('faction', 'delete') + + if current_folder_param.present? && delete_target.present? + team = participant_team + team.set_team_directory_num + + current_folder = clean_folder(current_folder_param) + base_path = team.path.to_s + directory_path = current_folder == '/' ? base_path : File.join(base_path, current_folder) + + return Array(delete_target).filter_map do |entry_name| + sanitized_name = clean_filename(entry_name) + File.join(directory_path, sanitized_name) if sanitized_name.present? + end + end + + Array(params[:chk_files]).filter_map do |idx| + directory = params.dig(:directories, idx) || params.dig(:directories, idx.to_s) + filename = params.dig(:filenames, idx) || params.dig(:filenames, idx.to_s) + next if directory.blank? || filename.blank? + + File.join(directory, filename) + end + end end From f7a6950b5acab34090adced6ff51971f2d051188 Mon Sep 17 00:00:00 2001 From: mcarthur-reece Date: Sat, 25 Apr 2026 10:35:09 -0400 Subject: [PATCH 10/10] fixing deletion for artifacts --- .DS_Store | Bin 0 -> 10244 bytes .../submitted_content_controller.rb | 79 ++++++++++++------ 2 files changed, 54 insertions(+), 25 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2cf7c700c37f8589a403061bb32cd0764249757f GIT binary patch literal 10244 zcmeHM&u<$=6nuY{09-l2d$a3iXYERGf<`ma&YPY2-rJdP_syG`1prnX zc3uQ10)T-RNpk_S8Hw8Ino60t=2?^s^+B|4SJ-%GPGNP0Wig=N%#&R4DV@kt$GDV>yl_)?i|h zD#}Qe#aj#(?;TjAj_uVLKaOOi4op57?Zi)J@eYN>TMt&0aA4I)>RdCR8JK2(_U>7* zU;u-LtL)!v*i!yWCaIPM8yxUbsBcrKIcS3b54wo$saSR+g)JnpPzE1{DUB~v8W&&# z`|3dtIHKtpE^t*RLlZ-?-x=tDox=FDF!m@I#OD{^*#p-J!b;_5mdWOxn9Cb^W8S#e zJP@O1XobU|X8CvMZbt~$o|G+n*WsgFu!o|(q(gT>B{2r;>ya(lTTf}cK!O(E%S

SUf!mqhQey8U-u2zHr?w5jdeEy#2cYa@+`czu|QSKJf_o-5gL;u*>}b<&@6e zzUBLt*WZ4N&2(YFM$Iu5?bpjv<|2pB|a2xISXS0*%aw1ia24H5}d@Sb22r=qtTPY z`JEi%SDcz2=5Q()V*DmIYO9VxF0Fj>3ih0Q_{x~{=YM`bf8|A|oWOu>Ts%^QoEh6F zS}BfDKztpe_y)%DI>xF#bX3=&IHhRJ00x-z~$Dwchkh-HN5*rOp%24yTCsQr=?K@y)s%m6h~6Ncm|iOVA{ z#8~R%7;s^(T9q87(IbhJi+B|+xk;jPXqgyIb4XkyaaqLSdPlV;qxo+M1`0TK;(+)r z(~2VfMkNPHjX5|%yeC&zzG_|aki_BwLnqC^S~eB|Lc$b|Nr<5NcU7Tpc(kr z46xira|3t2@+4uFHm+Eph}Xw>QKA!vaU|tI1!e@HdU+g=H*_2?k5I6#LT!|=T#e;8 bl30W3zxWxT_y0B$uMY>$+2P