chore: init commit
in worker.../repo/GITFOLDER.zip is the .git folder.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class AnnouncementsController < BaseController
|
||||
before_action :set_announcement, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
sort_column = params[:sort].presence || "created_at"
|
||||
sort_direction = params[:direction] == "desc" ? :desc : :asc
|
||||
|
||||
@announcements = Announcement.reorder(sort_column => sort_direction)
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: @announcement.title }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@announcement = Announcement.new
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: "New Announcement" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: @announcement.title, path: admin_announcement_path(@announcement) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@announcement = Announcement.new(announcement_params)
|
||||
|
||||
if @announcement.save
|
||||
redirect_to admin_announcement_path(@announcement), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: "New Announcement" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @announcement.update(announcement_params)
|
||||
redirect_to admin_announcement_path(@announcement), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Announcements", path: admin_announcements_path },
|
||||
{ label: @announcement.title, path: admin_announcement_path(@announcement) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@announcement.destroy
|
||||
redirect_to admin_announcements_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def announcement_params
|
||||
params.expect(announcement: %i[title content featured])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Base controller for in-house admin.
|
||||
# All Admin controllers inherit from this controller.
|
||||
module Admin
|
||||
class BaseController < ApplicationController
|
||||
layout "admin"
|
||||
|
||||
before_action :authenticate_admin
|
||||
|
||||
private
|
||||
|
||||
def authenticate_admin
|
||||
redirect_to root_path, alert: t("application.access_denied.admin_required") unless current_user&.admin?
|
||||
end
|
||||
|
||||
# Apply sorting to a collection based on params
|
||||
# @param collection [ActiveRecord::Relation] The base collection to sort
|
||||
# @param default [String] The default column to sort by
|
||||
# @return [ActiveRecord::Relation] The sorted collection
|
||||
def apply_sorting(collection, default:)
|
||||
sort_column = params[:sort].presence || default
|
||||
sort_direction = params[:direction] == "desc" ? :desc : :asc
|
||||
collection.reorder(sort_column => sort_direction)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ClassroomsController < BaseController
|
||||
before_action :set_classroom, only: %i[show edit update toggle_archive]
|
||||
|
||||
def index
|
||||
@classrooms = apply_sorting(Classroom.all, default: "name")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: @classroom.name }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@classroom = Classroom.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: "New Classroom" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: @classroom.name, path: admin_classroom_path(@classroom) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@classroom = Classroom.new(classroom_params)
|
||||
|
||||
if @classroom.save
|
||||
redirect_to admin_classroom_path(@classroom), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: "New Classroom" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @classroom.update(classroom_params)
|
||||
redirect_to admin_classroom_path(@classroom), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Classrooms", path: admin_classrooms_path },
|
||||
{ label: @classroom.name, path: admin_classroom_path(@classroom) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_archive
|
||||
authorize @classroom, :toggle_archive?
|
||||
@classroom.update!(archived: !@classroom.archived)
|
||||
flash[:notice] = @classroom.archived? ? "Classroom has been archived." : "Classroom has been activated."
|
||||
redirect_to admin_classroom_path(@classroom)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def classroom_params
|
||||
params.expect(classroom: [:name, :trading_enabled, :school_year_id, { grade_ids: [] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ComponentDemoController < BaseController
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def index
|
||||
# Demo data for table component - apply sorting before limit
|
||||
@users = User.all
|
||||
|
||||
# Apply sorting if params present
|
||||
if params[:sort].present?
|
||||
direction = params[:direction] == "desc" ? :desc : :asc
|
||||
@users = @users.order(params[:sort] => direction)
|
||||
end
|
||||
|
||||
@users = @users.limit(10)
|
||||
|
||||
# Demo data for show component
|
||||
@sample_user = User.first || User.new(
|
||||
email: "demo@example.com",
|
||||
name: "Demo User",
|
||||
admin: true,
|
||||
created_at: Time.current
|
||||
)
|
||||
|
||||
# Demo breadcrumbs
|
||||
@breadcrumbs = [
|
||||
{ label: "Components", path: admin_component_demo_index_path },
|
||||
{ label: "Demo" }
|
||||
]
|
||||
|
||||
# Demo table columns
|
||||
@columns = [
|
||||
{ attribute: :id, label: "ID", sortable: true },
|
||||
{ attribute: :email, label: "Email", sortable: true },
|
||||
{ attribute: :name, label: "Name", sortable: true },
|
||||
{ attribute: :admin, label: "Admin", sortable: true },
|
||||
{ attribute: :created_at, label: "Created", sortable: true }
|
||||
]
|
||||
|
||||
# Demo filters
|
||||
@filters = [
|
||||
{
|
||||
name: :type,
|
||||
label: "User Type",
|
||||
options: [["All", ""], ["Admin", "admin"], ["Teacher", "teacher"], ["Student", "student"]]
|
||||
},
|
||||
{
|
||||
name: :status,
|
||||
label: "Status",
|
||||
options: [["All", ""], ["Active", "active"], ["Inactive", "inactive"]]
|
||||
}
|
||||
]
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def show
|
||||
@user = User.find(params.expect(:id))
|
||||
@breadcrumbs = [
|
||||
{ label: "Components", path: admin_component_demo_index_path },
|
||||
{ label: "User Details" }
|
||||
]
|
||||
end
|
||||
|
||||
def form
|
||||
@user = User.first || User.new(email: "", name: "", admin: false)
|
||||
|
||||
# Add validation errors for demo purposes if requested
|
||||
if params[:show_errors] == "true"
|
||||
@user.errors.add(:email, "can't be blank")
|
||||
@user.errors.add(:name, "is too short (minimum is 3 characters)")
|
||||
end
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Components", path: admin_component_demo_index_path },
|
||||
{ label: "Form Builder Demo" }
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class DashboardController < BaseController
|
||||
def index
|
||||
# Dashboard will show overview of admin resources
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class PortfolioTransactionsController < BaseController
|
||||
before_action :set_portfolio_transaction, only: %i[show edit update destroy]
|
||||
|
||||
def show
|
||||
# TODO: FIX
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transaction ##{@portfolio_transaction.id}" }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@portfolio_transaction = PortfolioTransaction.new
|
||||
# TODO: FIX
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transactions", path: "#" },
|
||||
{ label: "New" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transaction ##{@portfolio_transaction.id}",
|
||||
path: admin_portfolio_transaction_path(@portfolio_transaction) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@portfolio_transaction = PortfolioTransaction.new(portfolio_transaction_params)
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
if @portfolio_transaction.save
|
||||
redirect_to admin_portfolio_transaction_path(@portfolio_transaction),
|
||||
notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transactions", path: "#" },
|
||||
{ label: "New" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
|
||||
if @portfolio_transaction.update(portfolio_transaction_params)
|
||||
redirect_to admin_portfolio_transaction_path(@portfolio_transaction),
|
||||
notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Portfolio Transaction ##{@portfolio_transaction.id}",
|
||||
path: admin_portfolio_transaction_path(@portfolio_transaction) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
# TODO: Determine if explicit authorization is needed since BaseController already restricts to admins
|
||||
# authorize @portfolio_transaction
|
||||
@portfolio_transaction.destroy
|
||||
|
||||
redirect_to admin_root_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_portfolio_transaction
|
||||
@portfolio_transaction = PortfolioTransaction.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def portfolio_transaction_params
|
||||
params.expect(portfolio_transaction: %i[portfolio_id transaction_type reason description amount_cents])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SchoolYearsController < BaseController
|
||||
before_action :set_school_year, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@school_years = apply_sorting(SchoolYear.includes(:school, :year), default: "id")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: @school_year.to_s }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@school_year = SchoolYear.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: "New School Year" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: @school_year.to_s, path: admin_school_year_path(@school_year) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
school = School.find_by(id: school_year_params[:school_id])
|
||||
year = Year.find_by(id: school_year_params[:year_id])
|
||||
|
||||
unless school && year
|
||||
@school_year = SchoolYear.new(school_year_params)
|
||||
@school_year.valid?
|
||||
return render_new_with_errors
|
||||
end
|
||||
|
||||
@school_year = SchoolYear.create!(school:, year:)
|
||||
|
||||
if @school_year.persisted?
|
||||
redirect_to admin_school_year_path(@school_year), notice: t(".notice")
|
||||
else
|
||||
render_new_with_errors
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
save_error(school_year_params)
|
||||
render_new_with_errors
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@school_year = e.record
|
||||
render_new_with_errors
|
||||
end
|
||||
|
||||
def update
|
||||
if @school_year.update(school_year_params)
|
||||
redirect_to admin_school_year_path(@school_year), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: @school_year.to_s, path: admin_school_year_path(@school_year) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @school_year.destroy
|
||||
redirect_to admin_school_years_path, notice: t(".notice")
|
||||
else
|
||||
redirect_to admin_school_years_path, alert: t(".delete_restricted")
|
||||
end
|
||||
rescue ActiveRecord::DeleteRestrictionError
|
||||
redirect_to admin_school_years_path, alert: t(".delete_restricted")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_school_year
|
||||
@school_year = SchoolYear.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def school_year_params
|
||||
params.expect(school_year: %i[school_id year_id])
|
||||
end
|
||||
|
||||
def render_new_with_errors
|
||||
@breadcrumbs = [
|
||||
{ label: "School Years", path: admin_school_years_path },
|
||||
{ label: "New School Year" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def save_error(params)
|
||||
@school_year = SchoolYear.new(params)
|
||||
@school_year.valid?
|
||||
@school_year.errors.add(:base, "A School year with this school and year already exists.")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SchoolsController < BaseController
|
||||
before_action :set_school, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
sort_column = params[:sort].presence || "name"
|
||||
sort_direction = params[:direction] == "desc" ? :desc : :asc
|
||||
|
||||
@schools = School.reorder(sort_column => sort_direction)
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: @school.name }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@school = School.new
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: "New School" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: @school.name, path: admin_school_path(@school) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
year_ids = school_params[:year_ids]&.reject(&:blank?)
|
||||
@school = School.new(school_params.except(:year_ids))
|
||||
@school.year_ids = year_ids if year_ids.present?
|
||||
|
||||
if @school.save
|
||||
redirect_to admin_school_path(@school), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: "New School" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
year_ids = school_params[:year_ids]&.reject(&:blank?)
|
||||
update_params = school_params.except(:year_ids)
|
||||
update_params[:year_ids] = year_ids || []
|
||||
|
||||
if @school.update(update_params)
|
||||
redirect_to admin_school_path(@school), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Schools", path: admin_schools_path },
|
||||
{ label: @school.name, path: admin_school_path(@school) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@school.destroy
|
||||
redirect_to admin_schools_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_school
|
||||
@school = School.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def set_form_data
|
||||
@years = Year.ordered_by_start_year
|
||||
end
|
||||
|
||||
def school_params
|
||||
params.expect(school: [:name, { year_ids: [] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class StocksController < BaseController
|
||||
before_action :set_stock, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@stocks = apply_sorting(Stock.all, default: "ticker")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: @stock.ticker }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@stock = Stock.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: "New Stock" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: @stock.ticker, path: admin_stock_path(@stock) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@stock = Stock.new(stock_params)
|
||||
|
||||
if @stock.save
|
||||
redirect_to admin_stock_path(@stock), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: "New Stock" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @stock.update(stock_params)
|
||||
redirect_to admin_stock_path(@stock), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Stocks", path: admin_stocks_path },
|
||||
{ label: @stock.ticker, path: admin_stock_path(@stock) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@stock.destroy
|
||||
redirect_to admin_stocks_path, notice: t(".notice")
|
||||
rescue ActiveRecord::DeleteRestrictionError => e
|
||||
redirect_to admin_stocks_path, alert: "Cannot delete stock: #{e.message}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_stock
|
||||
@stock = Stock.find(params.expect(:id))
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def stock_params
|
||||
params.expect(
|
||||
stock: %i[
|
||||
ticker
|
||||
company_name
|
||||
company_website
|
||||
stock_exchange
|
||||
description
|
||||
price_cents
|
||||
yesterday_price_cents
|
||||
employees
|
||||
industry
|
||||
cash_flow
|
||||
debt
|
||||
debt_to_equity
|
||||
profit_margin
|
||||
sales_growth
|
||||
industry_avg_debt_to_equity
|
||||
industry_avg_profit_margin
|
||||
industry_avg_sales_growth
|
||||
management
|
||||
competitor_names
|
||||
archived
|
||||
]
|
||||
)
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,235 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
# rubocop:disable Metrics/ClassLength
|
||||
class StudentsController < BaseController
|
||||
include SoftDeletableFiltering
|
||||
|
||||
before_action :set_student, only: %i[show edit update destroy add_transaction]
|
||||
before_action :set_discarded_student, only: %i[restore]
|
||||
|
||||
def index
|
||||
@students = apply_sorting(scoped_by_discard_status(Student), default: "username")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Students" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@attendance_entries = AttendanceEntryPresenter.for_student(@student)
|
||||
@earnings_summary = EarningsSummary.new(@student.portfolio) if @student.portfolio.present?
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: @student.username }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@student = Student.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: "New Student" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: @student.username, path: admin_student_path(@student) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@student = Student.new(student_params)
|
||||
|
||||
# Generate a memorable password if not provided
|
||||
if @student.password.blank?
|
||||
generated_password = MemorablePasswordGenerator.generate
|
||||
@student.password = generated_password
|
||||
@student.password_confirmation = generated_password
|
||||
else
|
||||
generated_password = @student.password
|
||||
end
|
||||
|
||||
if @student.save
|
||||
redirect_to admin_student_path(@student),
|
||||
notice: t(".notice", username: @student.username, password: generated_password)
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: "New Student" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @student.update(student_params)
|
||||
redirect_to admin_student_path(@student), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Students", path: admin_students_path },
|
||||
{ label: @student.username, path: admin_student_path(@student) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
username = @student.username
|
||||
@student.discard
|
||||
redirect_to admin_students_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
def restore
|
||||
username = @student.username
|
||||
@student.undiscard
|
||||
redirect_to admin_students_path(discarded: true), notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
def add_transaction
|
||||
errors = validate_transaction_params
|
||||
|
||||
if errors.present?
|
||||
redirect_to edit_admin_student_path(@student), alert: errors.join(", ")
|
||||
else
|
||||
transaction = PortfolioTransaction.new(
|
||||
portfolio: @student.portfolio,
|
||||
amount_cents: transaction_amount_cents,
|
||||
transaction_type: transaction_type,
|
||||
reason: transaction_reason,
|
||||
description: transaction_description
|
||||
)
|
||||
|
||||
if transaction.save
|
||||
redirect_to admin_student_path(@student), notice: t(".notice")
|
||||
else
|
||||
redirect_to edit_admin_student_path(@student), alert: transaction.errors.full_messages.join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def import
|
||||
return redirect_with_missing_file_error if params[:csv_file].blank?
|
||||
|
||||
begin
|
||||
results = BulkStudentImportService.import_from_csv(params.expect(:csv_file).path)
|
||||
redirect_with_import_results(results)
|
||||
rescue CSV::MalformedCSVError => e
|
||||
redirect_to admin_students_path, alert: "Invalid CSV format: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def template
|
||||
send_data BulkStudentImportService.generate_csv_template,
|
||||
filename: "student_import_template.csv",
|
||||
type: "text/csv",
|
||||
disposition: "attachment"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_discarded_student
|
||||
@student = Student.with_discarded.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def set_student
|
||||
@student = Student.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def student_params
|
||||
params.expect(student: %i[username classroom_id password password_confirmation])
|
||||
end
|
||||
|
||||
def transaction_params
|
||||
params.permit(:add_fund_amount, :transaction_type, :transaction_reason, :transaction_description)
|
||||
end
|
||||
|
||||
def transaction_amount_cents
|
||||
amount = transaction_params[:add_fund_amount]
|
||||
amount.present? ? (amount.to_f * 100).to_i : nil
|
||||
end
|
||||
|
||||
def transaction_type
|
||||
transaction_params[:transaction_type]
|
||||
end
|
||||
|
||||
def transaction_reason
|
||||
transaction_params[:transaction_reason]
|
||||
end
|
||||
|
||||
def transaction_description
|
||||
transaction_params[:transaction_description]
|
||||
end
|
||||
|
||||
def validate_transaction_params
|
||||
errors = []
|
||||
errors << t("admin.students.add_transaction.errors.transaction_type_blank") if transaction_type.blank?
|
||||
errors << t("admin.students.add_transaction.errors.amount_blank") if transaction_amount_cents.blank?
|
||||
errors << t("admin.students.add_transaction.errors.reason_blank") if transaction_reason.blank?
|
||||
errors
|
||||
end
|
||||
|
||||
def redirect_with_missing_file_error
|
||||
redirect_to admin_students_path, alert: t(".errors.no_file")
|
||||
end
|
||||
|
||||
def redirect_with_import_results(results)
|
||||
return redirect_with_no_results_error if results.empty?
|
||||
|
||||
created, skipped, failed = partition_results(results)
|
||||
success_messages = build_success_messages(created, skipped)
|
||||
|
||||
if failed.any?
|
||||
redirect_with_mixed_results(success_messages, failed)
|
||||
else
|
||||
redirect_to admin_students_path, notice: success_messages.join(". ")
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_with_no_results_error
|
||||
redirect_to admin_students_path, alert: t(".errors.no_students")
|
||||
end
|
||||
|
||||
def partition_results(results)
|
||||
[
|
||||
results.select(&:created?),
|
||||
results.select(&:skipped?),
|
||||
results.select(&:failed?)
|
||||
]
|
||||
end
|
||||
|
||||
def build_success_messages(created, skipped)
|
||||
messages = []
|
||||
messages << build_created_message(created) if created.any?
|
||||
messages << build_skipped_message(skipped) if skipped.any?
|
||||
messages
|
||||
end
|
||||
|
||||
def build_created_message(created)
|
||||
usernames = created.map { |item| item.student.username }
|
||||
"Successfully created #{created.count} students: #{usernames.join(', ')}"
|
||||
end
|
||||
|
||||
def build_skipped_message(skipped)
|
||||
"Skipped #{skipped.count} existing usernames"
|
||||
end
|
||||
|
||||
def redirect_with_mixed_results(success_messages, failed)
|
||||
error_messages = failed.map { |item| "Row #{item.line_number}: #{item.error_message}" }
|
||||
alert_message = "#{failed.count} errors occurred: #{error_messages.join(', ')}"
|
||||
|
||||
if success_messages.any?
|
||||
redirect_to admin_students_path, notice: success_messages.join(". "), alert: alert_message
|
||||
else
|
||||
redirect_to admin_students_path, alert: alert_message
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/ClassLength
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module Teachers
|
||||
class DeactivationsController < BaseController
|
||||
before_action :set_teacher
|
||||
|
||||
def create
|
||||
username = @teacher.username
|
||||
@teacher.discard
|
||||
redirect_back_or_to admin_teachers_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_teacher
|
||||
@teacher = Teacher.find(params.expect(:teacher_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
module Teachers
|
||||
class ReactivationsController < BaseController
|
||||
before_action :set_teacher
|
||||
|
||||
def create
|
||||
username = @teacher.username
|
||||
@teacher.undiscard
|
||||
redirect_back_or_to admin_teachers_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_teacher
|
||||
@teacher = Teacher.with_discarded.find(params.expect(:teacher_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class TeachersController < BaseController
|
||||
include SoftDeletableFiltering
|
||||
|
||||
before_action :set_teacher, only: %i[show edit update destroy]
|
||||
before_action :require_deactivated, only: %i[destroy]
|
||||
|
||||
def index
|
||||
@teachers = apply_sorting(scoped_by_discard_status(Teacher), default: "username")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: @teacher.username }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@teacher = Teacher.new
|
||||
set_form_data
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: "New Teacher" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: @teacher.username, path: admin_teacher_path(@teacher) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
temp_password = Devise.friendly_token.first(20)
|
||||
classroom_ids = teacher_params[:classroom_ids]&.reject(&:blank?)
|
||||
|
||||
@teacher = Teacher.new(teacher_params.except(:classroom_ids).merge(password: temp_password))
|
||||
@teacher.classroom_ids = classroom_ids if classroom_ids.present?
|
||||
|
||||
if @teacher.save
|
||||
@teacher.send_reset_password_instructions
|
||||
redirect_to admin_teacher_path(@teacher), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: "New Teacher" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
classroom_ids = teacher_params[:classroom_ids]&.reject(&:blank?)
|
||||
update_params = teacher_params.except(:classroom_ids)
|
||||
update_params[:classroom_ids] = classroom_ids if classroom_ids.present?
|
||||
|
||||
if @teacher.update(update_params)
|
||||
redirect_to admin_teacher_path(@teacher), notice: t(".notice")
|
||||
else
|
||||
set_form_data
|
||||
@breadcrumbs = [
|
||||
{ label: "Teachers", path: admin_teachers_path },
|
||||
{ label: @teacher.username, path: admin_teacher_path(@teacher) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
username = @teacher.username
|
||||
@teacher.really_destroy!
|
||||
redirect_to admin_teachers_path, notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_teacher
|
||||
@teacher = Teacher.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def require_deactivated
|
||||
return if @teacher.discarded?
|
||||
|
||||
redirect_to edit_admin_teacher_path(@teacher), alert: t("admin.teachers.destroy.must_be_deactivated")
|
||||
end
|
||||
|
||||
def teacher_params
|
||||
params.expect(teacher: [:email, :name, { classroom_ids: [] }])
|
||||
end
|
||||
|
||||
def set_form_data
|
||||
active_years = Year.current_school_year(Date.current)
|
||||
@schools = School.joins(:school_years).where(school_years: { year_id: active_years.ids }).distinct.order(:name)
|
||||
|
||||
@selected_school_id = params[:school_id] || @teacher.classrooms.first&.school&.id
|
||||
|
||||
@classrooms = Classroom.active.joins(:school_year).where(school_years: { year_id: active_years.ids })
|
||||
@classrooms = @classrooms.where(school_years: { school_id: @selected_school_id }) if @selected_school_id.present?
|
||||
@classrooms = @classrooms.order(:name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class UsersController < BaseController
|
||||
before_action :set_user, only: %i[show edit update destroy]
|
||||
|
||||
def index
|
||||
@users = apply_sorting(User.all, default: "username")
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Users" }
|
||||
]
|
||||
end
|
||||
|
||||
def show
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: @user.username }
|
||||
]
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: "New User" }
|
||||
]
|
||||
end
|
||||
|
||||
def edit
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: @user.username, path: admin_user_path(@user) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
|
||||
if @user.save
|
||||
redirect_to admin_user_path(@user), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: "New User" }
|
||||
]
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @user.update(user_params)
|
||||
redirect_to admin_user_path(@user), notice: t(".notice")
|
||||
else
|
||||
@breadcrumbs = [
|
||||
{ label: "Users", path: admin_users_path },
|
||||
{ label: @user.username, path: admin_user_path(@user) },
|
||||
{ label: "Edit" }
|
||||
]
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.destroy
|
||||
redirect_to admin_users_path, notice: t(".notice")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.expect(user: %i[username email type admin classroom_id password password_confirmation])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AnnouncementsController < ApplicationController
|
||||
before_action :set_announcement, only: %i[show]
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def set_announcement
|
||||
@announcement = Announcement.find(params.expect(:id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path, alert: t("announcements.not_found")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Pundit::Authorization
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
before_action :set_navbar_stocks
|
||||
|
||||
protected
|
||||
|
||||
def ensure_teacher_or_admin
|
||||
authorize :application, :teacher_or_admin_required?
|
||||
end
|
||||
|
||||
def ensure_admin
|
||||
authorize :application, :admin_required?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_navbar_stocks
|
||||
@navbar_stocks = policy_scope(Stock).active
|
||||
end
|
||||
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: %i[username type classroom_id])
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: %i[username email])
|
||||
end
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError do
|
||||
if current_user.nil?
|
||||
# go to the login page
|
||||
redirect_to new_user_session_path, alert: t("devise.failure.unauthenticated")
|
||||
elsif current_user.student?
|
||||
redirect_to current_user&.portfolio_path, alert: t("application.access_denied.no_access")
|
||||
else
|
||||
redirect_to root_url, alert: t("application.access_denied.no_access")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Manages student enrollments in classrooms
|
||||
class ClassroomEnrollmentsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_teacher_or_admin
|
||||
before_action :set_classroom
|
||||
before_action :set_enrollment, only: %i[destroy unenroll]
|
||||
|
||||
# POST /classrooms/:classroom_id/classroom_enrollments
|
||||
def create
|
||||
student = Student.find(enrollment_params[:student_id])
|
||||
|
||||
enrollment = student.enroll_in!(
|
||||
@classroom,
|
||||
primary: enrollment_params[:primary] == "true"
|
||||
)
|
||||
|
||||
if enrollment.persisted?
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: "#{student.username} enrolled in #{@classroom.name}"
|
||||
else
|
||||
redirect_to classroom_path(@classroom),
|
||||
alert: "Failed to enroll student: #{enrollment.errors.full_messages.join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /classrooms/:classroom_id/classroom_enrollments/:id
|
||||
def destroy
|
||||
student = @enrollment.student
|
||||
@enrollment.destroy
|
||||
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: "Removed #{student.username}'s enrollment from #{@classroom.name}"
|
||||
end
|
||||
|
||||
# PATCH /classrooms/:classroom_id/classroom_enrollments/:id/unenroll
|
||||
def unenroll
|
||||
student = @enrollment.student
|
||||
@enrollment.unenroll!
|
||||
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: "#{student.username} unenrolled from #{@classroom.name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.find(params.expect(:classroom_id))
|
||||
end
|
||||
|
||||
def set_enrollment
|
||||
@enrollment = @classroom.classroom_enrollments.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def enrollment_params
|
||||
params.expect(classroom_enrollment: %i[student_id primary])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,110 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ClassroomsController < ApplicationController
|
||||
before_action :set_classroom, only: %i[show edit update toggle_trading]
|
||||
before_action :authorize_classroom, except: %i[edit update toggle_trading]
|
||||
before_action :authorize_classroom_instance, only: %i[edit update toggle_trading]
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_teacher_or_admin, except: %i[index show]
|
||||
before_action :check_classroom_eligibility, only: :show
|
||||
|
||||
def index
|
||||
@classrooms = policy_scope(Classroom).includes(:teachers, students: :portfolio)
|
||||
@classrooms = Classroom.apply_sorting(@classrooms, params[:sort], params[:direction])
|
||||
end
|
||||
|
||||
def show
|
||||
facade = ClassroomFacade.new(@classroom)
|
||||
@students = facade.students
|
||||
@can_manage_students = current_user.teacher_or_admin?
|
||||
@classroom_stats = facade.stats if @can_manage_students
|
||||
end
|
||||
|
||||
def new
|
||||
@classroom = Classroom.new
|
||||
dropdown_data
|
||||
end
|
||||
|
||||
def edit
|
||||
dropdown_data
|
||||
end
|
||||
|
||||
def create
|
||||
@classroom = Classroom.new(classroom_params.except(:school_id, :year_id))
|
||||
assign_school_year_to_classroom
|
||||
|
||||
if @classroom.save
|
||||
redirect_to classroom_url(@classroom), notice: t(".notice")
|
||||
else
|
||||
dropdown_data
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
assign_school_year_to_classroom
|
||||
|
||||
if @classroom.update(classroom_params.except(:school_id, :year_id))
|
||||
redirect_to classroom_url(@classroom), notice: t(".notice")
|
||||
else
|
||||
dropdown_data
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_trading
|
||||
if @classroom.update(trading_enabled: !@classroom.trading_enabled)
|
||||
notice_key = @classroom.trading_enabled? ? :enabled : :disabled
|
||||
redirect_to classroom_url(@classroom), notice: t(".#{notice_key}")
|
||||
else
|
||||
redirect_to classroom_url(@classroom), alert: t(".alert")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.includes(users: :portfolio).find(params.expect(:id).to_i)
|
||||
end
|
||||
|
||||
def authorize_classroom
|
||||
authorize Classroom
|
||||
end
|
||||
|
||||
def authorize_classroom_instance
|
||||
authorize @classroom
|
||||
end
|
||||
|
||||
def classroom_params
|
||||
params.expect(classroom: [:name, :trading_enabled, :school_id, :year_id, { grade_ids: [] }, { teacher_ids: [] }])
|
||||
end
|
||||
|
||||
def dropdown_data
|
||||
@schools = School.order(:name)
|
||||
@years = Year.order(:name)
|
||||
@teachers = Teacher.all.sort_by(&:display_name)
|
||||
end
|
||||
|
||||
def assign_school_year_to_classroom
|
||||
return unless classroom_params[:school_id].present? && classroom_params[:year_id].present?
|
||||
|
||||
school = School.find(classroom_params[:school_id])
|
||||
year = Year.find(classroom_params[:year_id])
|
||||
school_year = SchoolYear.find_or_create_by!(school: school, year: year)
|
||||
@classroom.school_year = school_year
|
||||
end
|
||||
|
||||
def check_classroom_eligibility
|
||||
# Redirect if classroom is archived and user is not an admin
|
||||
if @classroom.archived? && !current_user.admin?
|
||||
redirect_to root_path, alert: t("classrooms.archived.alert")
|
||||
return
|
||||
end
|
||||
|
||||
return if current_user.admin? ||
|
||||
(current_user.teacher? &&
|
||||
@classroom.teachers.include?(current_user))
|
||||
|
||||
redirect_to root_path, alert: t("application.access_denied.no_access")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SoftDeletableFiltering
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
# Scopes a resource class based on discard status query parameters
|
||||
#
|
||||
# @param resource_class [ActiveRecord::Base] The model class to scope
|
||||
# @return [ActiveRecord::Relation] Scoped collection based on params
|
||||
#
|
||||
# Query parameters:
|
||||
# - discarded (any value): Returns only discarded records
|
||||
# - all (any value): Returns all records (including discarded)
|
||||
# - (none): Returns only kept (non-discarded) records
|
||||
def scoped_by_discard_status(resource_class)
|
||||
if params[:discarded].present?
|
||||
resource_class.discarded
|
||||
elsif params[:all].present?
|
||||
resource_class.with_discarded
|
||||
else
|
||||
resource_class.kept
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# app/controllers/grade_books_controller.rb
|
||||
class GradeBooksController < ApplicationController
|
||||
before_action :set_classroom_and_grade_book
|
||||
before_action :authorize_grade_book
|
||||
def show; end
|
||||
|
||||
def update
|
||||
GradeEntry.transaction do
|
||||
grade_entry_params.each do |id, attrs|
|
||||
entry = @grade_book.grade_entries.find(id)
|
||||
entry.update!(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@grade_book.grade_entries.reload
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to classroom_grade_book_path(@classroom, @grade_book), notice: t(".notice") }
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
def finalize
|
||||
if @grade_book.completed?
|
||||
redirect_to classroom_grade_book_path(@classroom, @grade_book),
|
||||
alert: t(".already_completed")
|
||||
else
|
||||
@grade_book.verified!
|
||||
DistributeEarnings.execute(@grade_book)
|
||||
redirect_to classroom_grade_book_path(@classroom, @grade_book),
|
||||
notice: t(".notice")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_grade_book
|
||||
authorize @grade_book
|
||||
end
|
||||
|
||||
def set_classroom_and_grade_book
|
||||
@classroom = Classroom.find(params.expect(:classroom_id))
|
||||
@grade_book = @classroom.grade_books.includes(grade_entries: :user).find(params.expect(:id))
|
||||
|
||||
# Redirect if classroom is archived and user is not an admin
|
||||
return unless @classroom.archived? && !current_user.admin?
|
||||
|
||||
redirect_to root_path, alert: t("classrooms.archived.alert")
|
||||
end
|
||||
|
||||
def grade_entry_params
|
||||
params.require(:grade_entries).transform_values do |entry|
|
||||
entry.permit(:math_grade, :reading_grade, :attendance_days, :is_perfect_attendance)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
@current_announcement = Announcement.current
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrdersController < ApplicationController
|
||||
before_action :set_order, only: %i[edit update cancel]
|
||||
before_action :set_stock, only: %i[new]
|
||||
before_action :set_shared_owned, only: %i[new edit]
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@orders = policy_scope(Order)
|
||||
@orders = Order.apply_sorting(@orders, params[:sort], params[:direction])
|
||||
end
|
||||
|
||||
def new
|
||||
@order = Order.new(action: params[:transaction_type], stock: @stock)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@order = Order.new(order_params.merge(user: current_user))
|
||||
|
||||
respond_to do |format|
|
||||
if @order.save
|
||||
format.html { redirect_to orders_url, notice: t(".notice") }
|
||||
format.turbo_stream { redirect_to orders_url, notice: t(".notice") }
|
||||
format.json { render :show, status: :created, location: @order }
|
||||
else
|
||||
setup_error_data
|
||||
format.html { render :new, status: :unprocessable_content }
|
||||
format.turbo_stream { render :new, status: :unprocessable_content }
|
||||
format.json { render json: @order.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @order.update(order_params)
|
||||
format.html { redirect_to orders_url, notice: t(".notice") }
|
||||
format.json { render :show, status: :ok, location: @order }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_content }
|
||||
format.json { render json: @order.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cancel
|
||||
authorize @order
|
||||
|
||||
if @order.pending?
|
||||
cancel_order
|
||||
else
|
||||
invalid_order_response
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Strong parameters
|
||||
def order_params
|
||||
params.expect(order: %i[stock_id shares action])
|
||||
end
|
||||
|
||||
def set_order
|
||||
@order = Order.find(params.expect(:id))
|
||||
end
|
||||
|
||||
def set_stock
|
||||
@stock = Stock.find(params.expect(:stock_id))
|
||||
end
|
||||
|
||||
def set_shared_owned
|
||||
@shares_owned = current_user.portfolio&.shares_owned(@stock&.id)
|
||||
end
|
||||
|
||||
def setup_error_data
|
||||
@stock = @order.stock
|
||||
@shares_owned = current_user.portfolio&.shares_owned(@stock&.id)
|
||||
end
|
||||
|
||||
def unauthorized_response
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, alert: t(".unauthorized") }
|
||||
format.json { render json: { error: t(".unauthorized") }, status: :forbidden }
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_order
|
||||
@order.cancel!
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, notice: t(".success") }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_order_response
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, alert: t(".pending_only") }
|
||||
format.json { render json: { error: t(".pending_only") }, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@order.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to orders_url, notice: t(".notice") }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PortfoliosController < ApplicationController
|
||||
before_action :set_portfolio
|
||||
before_action :authenticate_user!
|
||||
|
||||
def show
|
||||
authorize @portfolio
|
||||
@stocks = @portfolio.stocks
|
||||
@earnings_summary = EarningsSummary.new(@portfolio)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_portfolio
|
||||
@portfolio = Portfolio.find(params.expect(:id))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SchoolsController < ApplicationController
|
||||
before_action :set_school, only: %i[show edit update destroy]
|
||||
before_action :authenticate_user!
|
||||
|
||||
# GET /schools or /schools.json
|
||||
def index
|
||||
@schools = School.all
|
||||
end
|
||||
|
||||
# GET /schools/1 or /schools/1.json
|
||||
def show; end
|
||||
|
||||
# GET /schools/new
|
||||
def new
|
||||
@school = School.new
|
||||
end
|
||||
|
||||
# GET /schools/1/edit
|
||||
def edit; end
|
||||
|
||||
# POST /schools or /schools.json
|
||||
def create
|
||||
@school = School.new(school_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @school.save
|
||||
format.html { redirect_to school_url(@school), notice: t(".notice") }
|
||||
format.json { render :show, status: :created, location: @school }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_content }
|
||||
format.json { render json: @school.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /schools/1 or /schools/1.json
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @school.update(school_params)
|
||||
format.html { redirect_to school_url(@school), notice: t(".notice") }
|
||||
format.json { render :show, status: :ok, location: @school }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_content }
|
||||
format.json { render json: @school.errors, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /schools/1 or /schools/1.json
|
||||
def destroy
|
||||
@school.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to schools_url, notice: t(".notice") }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_school
|
||||
@school = School.find(params.expect(:id))
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def school_params
|
||||
params.expect(school: [:name])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StocksController < ApplicationController
|
||||
before_action :set_stock, only: %i[show]
|
||||
before_action :authenticate_user!
|
||||
before_action :set_portfolio
|
||||
|
||||
def index
|
||||
@stocks = policy_scope(Stock).includes(portfolio_stocks: :portfolio)
|
||||
@portfolio = current_user.portfolio if current_user.student?
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @stock
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_portfolio
|
||||
@portfolio = current_user.portfolio
|
||||
end
|
||||
|
||||
def set_stock
|
||||
@stock = Stock.find(params.expect(:id))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StudentsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_teacher_or_admin
|
||||
before_action :set_classroom
|
||||
before_action :set_student, except: %i[new create]
|
||||
|
||||
def new
|
||||
@student = Student.new(classroom: @classroom)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
@student = Student.new(student_params)
|
||||
@student.classroom = @classroom
|
||||
@student.password = generate_memorable_password
|
||||
|
||||
if @student.save
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: t(".notice", username: @student.username, password: @student.password)
|
||||
else
|
||||
render :new, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @student.update(student_params)
|
||||
redirect_to classroom_path(@classroom), notice: t(".notice")
|
||||
else
|
||||
render :edit, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
username = @student.username
|
||||
@student.discard
|
||||
redirect_to classroom_path(@classroom), notice: t(".notice", username: username)
|
||||
end
|
||||
|
||||
def reset_password
|
||||
new_password = generate_memorable_password
|
||||
@student.update!(password: new_password)
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: t(".notice", username: @student.username, password: new_password)
|
||||
end
|
||||
|
||||
def generate_password
|
||||
new_password = generate_memorable_password
|
||||
@student.update!(password: new_password)
|
||||
redirect_to classroom_path(@classroom),
|
||||
notice: t(".notice", username: @student.username, password: new_password)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_classroom
|
||||
@classroom = Classroom.find(params.expect(:classroom_id))
|
||||
|
||||
# Redirect if classroom is archived and user is not an admin
|
||||
return unless @classroom.archived? && !current_user.admin?
|
||||
|
||||
redirect_to root_path, alert: t("classrooms.archived.alert")
|
||||
end
|
||||
|
||||
def set_student
|
||||
@student = @classroom.users.students.kept.find(params.expect(:id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to classroom_path(@classroom), alert: t("students.not_found")
|
||||
end
|
||||
|
||||
def student_params
|
||||
params.expect(student: %i[username email])
|
||||
end
|
||||
|
||||
def generate_memorable_password
|
||||
MemorablePasswordGenerator.generate
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user