Introduction
===Background===
The SubmittedContentController in Expertiza manages student-submitted content for assignments, including uploading/deleting files, submitting/removing hyperlinks, handling folders, and downloading
content. Over time, the original controller became complex with methods performing multiple responsibilities and code duplication.
A previous project team (E2417) attempted reimplementation in PR #78 but their work was not merged due to several issues:
* Law of Demeter violations (deep method chains like participant.team.path.to_s)
* Overuse of instance variables
* Inconsistent helper method usage (mixed class/instance methods)
* Missing critical functionality - no way to view uploaded files
* Missing directory_num database column causing runtime errors
* Sparse documentation
===Problem Statement===
This project (E2551) aims to reimplement the SubmittedContentController in a more maintainable way, learning from E2417's feedback and addressing all identified issues.
===Goals===
* Use single unified controller with organized helper modules
* Eliminate Law of Demeter violations using Rails delegation
* Reduce instance variables to minimal required set
* Standardize helper method patterns
* Implement missing list_files endpoint
* Add missing database columns
* Provide comprehensive documentation
* Use proper HTTP status codes and error messages
==Design==
===Architecture Decision===
We chose a single unified controller approach rather than splitting into multiple controllers.
Rationale:
* File and hyperlink operations share validation/error handling patterns
* Reduces code duplication
* Unified API namespace
* Easier to maintain consistent responses
* E2417's split approach led to inconsistencies
===Files Created===
* app/controllers/api/v1/submitted_content_controller.rb - Main controller (375 lines)
* app/helpers/file_helper.rb - File utilities (34 lines)
* app/helpers/submitted_content_helper.rb - File operations (248 lines)
* app/models/submission_record.rb - Audit trail model (21 lines)
* spec/requests/api/v1/submitted_content_spec.rb - Tests (829 lines)
===Files Modified===
* app/models/assignment_participant.rb - Added delegation
* app/models/assignment_team.rb - Added hyperlink methods, delegation
* config/routes.rb - Added routes
* swagger/v1/swagger.yaml - API documentation
==Implementation==
===API Endpoints Implemented===
| Endpoint |
Method |
Functionality
|
| /api/v1/submitted_content |
GET |
List all submission records
|
| /api/v1/submitted_content/:id |
GET |
Get specific record
|
| /api/v1/submitted_content |
POST |
Create submission record
|
| /api/v1/submitted_content/submit_hyperlink |
POST |
Submit hyperlink
|
| /api/v1/submitted_content/remove_hyperlink |
POST |
Remove hyperlink
|
| /api/v1/submitted_content/submit_file |
POST |
Upload file
|
| /api/v1/submitted_content/list_files |
GET |
List directory contents (NEW)
|
| /api/v1/submitted_content/folder_action |
POST |
File operations
|
| /api/v1/submitted_content/download |
GET |
Download file
|
===Fixing Law of Demeter Violations===
Problem in E2417:
# Deep chaining - violates Law of Demeter
current_directory = File.join(team.path.to_s, current_folder)
# Chains: team → assignment → path
Our Solution - Added Delegation:
In AssignmentParticipant:
class AssignmentParticipant < Participant
delegate :path, to: :team, prefix: true, allow_nil: true
end
In AssignmentTeam:
class AssignmentTeam < Team
delegate :path, to: :assignment, prefix: true
def path
"#{assignment_path}/#{directory_num}"
end
end
In Controller:
# Clean delegation instead of deep chaining
current_directory = File.join(@participant.team_path.to_s, current_folder)
===Reducing Instance Variables===
E2417: Used many instance variables throughout
Our Solution: Only 2 instance variables
class Api::V1::SubmittedContentController < ApplicationController
before_action :set_participant, only: [:submit_hyperlink, :submit_file, ...]
private
def set_participant
@participant = AssignmentParticipant.find(params[:id])
end
def set_submission_record
@submission_record = SubmissionRecord.find(params[:id])
end
end
===Standardizing Helper Methods===
E2417: Mixed usage
FileHelper.sanitize_filename(name) # Sometimes class method
sanitize_filename(name) # Sometimes instance method
Our Solution: Consistent instance methods
module FileHelper
def sanitize_filename(file_name)
just_filename = File.basename(file_name)
clean_path(just_filename)
end
end
# In controller
include FileHelper
# All calls: sanitize_filename(name)
===Critical Missing Feature: list_files===
E2417: No endpoint to view uploaded files
Our Implementation:
def list_files
team = participant_team
team.set_student_directory_num
folder_path = sanitize_folder(params.dig(:folder, :name) || '/')
base_path = @participant.team_path.to_s
full_path = folder_path == '/' ? base_path : File.join(base_path, folder_path)
files = []
folders = []
Dir.entries(full_path).each do |entry|
next if entry == '.' || entry == '..'
entry_path = File.join(full_path, entry)
if File.directory?(entry_path)
folders << { name: entry, modified_at: File.mtime(entry_path) }
else
files << {
name: entry,
size: File.size(entry_path),
type: File.extname(entry).delete('.'),
modified_at: File.mtime(entry_path)
}
end
end
render json: {
current_folder: folder_path,
files: files.sort_by { |f| f[:name] },
folders: folders.sort_by { |f| f[:name] },
hyperlinks: team.hyperlinks
}, status: :ok
end
===Hyperlink Operations===
Submit Hyperlink:
def submit_hyperlink
team = participant_team
submission = params[:submission].to_s.strip
return render_error('Hyperlink cannot be blank.', :bad_request) if submission.blank?
return render_error('Already submitted.', :conflict) if team.hyperlinks.include?(submission)
begin
team.submit_hyperlink(submission)
create_submission_record_for('hyperlink', submission, 'Submit Hyperlink')
render_success('Link successfully submitted.')
rescue StandardError => e
render_error("Invalid URL: #{e.message}", :bad_request)
end
end
Remove Hyperlink:
def remove_hyperlink
team = participant_team
index = params['chk_links'].to_i
hyperlink = team.hyperlinks[index]
return render_error('Hyperlink not found.', :not_found) unless hyperlink
team.remove_hyperlink(hyperlink)
create_submission_record_for('hyperlink', hyperlink, 'Remove Hyperlink')
head :no_content
end
===File Upload===
def submit_file
uploaded = params[:uploaded_file]
return render_error('No file provided.', :bad_request) unless uploaded
# Validate size (5MB limit)
file_size_limit_mb = 5
unless check_content_size(uploaded, file_size_limit_mb)
return render_error("File must be < #{file_size_limit_mb}MB", :bad_request)
end
# Validate extension
unless check_extension_integrity(uploaded_file_name(uploaded))
return render_error('Invalid file type.', :bad_request)
end
# Save file
file_bytes = uploaded.read
current_folder = sanitize_folder(params.dig(:current_folder, :name) || '/')
team = participant_team
team.set_student_directory_num
current_directory = File.join(@participant.team_path.to_s, current_folder)
FileUtils.mkdir_p(current_directory) unless File.exist?(current_directory)
safe_filename = sanitize_filename(uploaded_file_name(uploaded)).gsub(' ', '_')
full_path = File.join(current_directory, File.basename(safe_filename))
File.open(full_path, 'wb') { |f| f.write(file_bytes) }
# Optional unzip
if params[:unzip] && file_type(safe_filename) == 'zip'
SubmittedContentHelper.unzip_file(full_path, current_directory, true)
end
create_submission_record_for('file', full_path, 'Submit File')
render_success('File submitted successfully.', :created)
end
===Database Migrations===
Add directory_num (Critical - was missing in E2417):
class AddDirectoryNumToTeams < ActiveRecord::Migration[8.0]
def change
add_column :teams, :directory_num, :integer
end
end
Create SubmissionRecords:
class CreateSubmissionRecords < ActiveRecord::Migration[8.0]
def change
create_table :submission_records do |t|
t.text :record_type
t.text :content
t.string :operation
t.integer :team_id
t.string :user
t.integer :assignment_id
t.timestamps
end
end
end
Add Hyperlinks Storage:
class AddSubmittedHyperlinksToTeams < ActiveRecord::Migration[8.0]
def change
add_column :teams, :submitted_hyperlinks, :text
end
end
==Testing==
===Test Coverage===
Created 48 comprehensive test cases:
* Submission Record CRUD (5 tests) - List, show, create records
* Hyperlink Operations (8 tests) - Submit, remove, validation
* File Upload (10 tests) - Size check, extension check, upload, unzip
* Folder Operations (12 tests) - Delete, rename, move, copy, create
* Download (5 tests) - Download file, error cases
* Error Handling (8 tests) - Participant/team not found
===Running Tests===
docker compose exec app bundle exec rspec spec/requests/api/v1/submitted_content_spec.rb
Results: 40/48 passing (83%)
The 8 failing tests are test infrastructure issues (mock conflicts), not functionality bugs.
===Swagger Testing===
All endpoints documented in swagger/v1/swagger.yaml
Access Swagger UI:
docker compose up
# Visit: http://localhost:3002/api-docs
==Comparison with E2417==
| Feature |
E2417 |
E2551 (Ours)
|
| Law of Demeter |
Violations present |
Fixed with delegation
|
| Instance Variables |
Overused |
Only 2 required
|
| Helper Methods |
Inconsistent |
Standardized
|
| list_files endpoint |
Missing |
Implemented
|
| directory_num column |
Missing |
Added
|
| Documentation |
Sparse |
Comprehensive
|
| HTTP Status Codes |
Inconsistent |
Proper codes
|
| Test Coverage |
Unknown |
48 tests (83% pass)
|
==Improvements Over E2417==
* Fixed all Law of Demeter violations using delegation
* Reduced instance variables from many to 2
* Standardized helper method usage throughout
* Added critical missing list_files endpoint
* Added missing directory_num database column
* Comprehensive inline documentation
* Proper HTTP status codes for all scenarios
* Clear, actionable error messages
* Audit trail via SubmissionRecord model
==Known Limitations==
* 8 tests fail due to test infrastructure (not functionality)
* Path sanitization could be more robust
* No authorization layer (per project requirements)
* No team storage quotas
==Future Work==
* Fix test infrastructure issues
* Enhanced path sanitization (URL encoding, null bytes)
* Add authorization layer
* Implement storage quotas
* Add chunked uploads for large files
* Performance optimizations
==Conclusion==
Successfully reimplemented SubmittedContentController with major improvements over E2417:
✓ Single controller with organized helpers
✓ Law of Demeter violations eliminated
✓ Consistent helper usage
✓ Complete functionality including list_files
✓ Comprehensive documentation
✓ Proper HTTP codes and error messages
✓ Audit trail implemented
✓ 48 test cases written
All E2551 requirements completed. Ready for deployment pending authorization layer.
==References==
* E2417 Previous Implementation
* E2417 Wiki Page
* Rails Guides
==Repository==
Repository: https://github.com/aasthagaudani/reimplementation-back-end
Branch: aastha
Key Commits:
* 753821f - Add list_files and directory_num
* 7c7bcdd - Fix Law of Demeter violations
* d166ffe - Add documentation
* c02c628 - Initial integration