-
class ActivitiesController < ApplicationController
-
before_action :set_resources
-
-
def index
-
authorize! with: ActivityPolicy
-
-
@pagy, @activities = pagy(@resources)
-
end
-
-
private
-
-
def set_resources
-
@resources =
-
if params.key?(:user_id)
-
PublicActivity::Activity.where(owner_type: 'User', owner_id: params[:user_id])
-
else
-
PublicActivity::Activity.all
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# TODO: Only accept json. Only respond as json. Force.
-
class ApplicationController < ActionController::API
-
include ActionController::MimeResponds
-
include ActionPolicy::Controller
-
include Pagy::Backend
-
include PublicActivity::StoreController
-
-
before_action :set_paper_trail_whodunnit
-
before_action :authenticate_user!
-
before_action :configure_permitted_parameters, if: :devise_controller?
-
-
authorize :user, through: :current_user
-
-
impersonates :user
-
-
respond_to :json
-
-
rescue_from ActionController::UnknownFormat do |ex|
-
response.headers['x-exception-message'] = ex.message unless Rails.env.production?
-
head :bad_request
-
end
-
-
private
-
-
def configure_permitted_parameters
-
devise_parameter_sanitizer.permit(:sign_up,
-
keys: [
-
:first_name, :last_name, :terms_of_service, :access_code,
-
user_schools_attributes: %i[school_id]
-
])
-
end
-
-
def user_for_paper_trail
-
user_signed_in? ? true_user.id : 'Public'
-
end
-
end
-
# frozen_string_literal: true
-
-
class ConfigurablesController < ApplicationController
-
before_action :set_keys
-
-
def show
-
authorize!
-
end
-
-
def update
-
authorize!
-
-
failures = Configurable
-
.keys.filter_map do |key|
-
params.key?(key) &&
-
Configurable.find_or_create_by(name: key)
-
end.reject do |configurable|
-
configurable.value = params[configurable.name]
-
configurable.save
-
end
-
-
if failures.empty?
-
render :show
-
else
-
render json: failures.flat_map(&:errors), status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def set_keys
-
@keys = Configurable.keys
-
end
-
end
-
# frozen_string_literal: true
-
-
class DistrictsController < ApplicationController
-
before_action :set_resources, only: %i[index]
-
before_action :set_district, only: %i[show update destroy]
-
-
def index
-
authorize!
-
-
@pagy, @districts = pagy(@resources)
-
end
-
-
def create
-
@district = District.new(district_params)
-
-
authorize!
-
-
if @district.save
-
render :show, status: :created
-
else
-
render json: @district.errors, status: :unprocessable_entity
-
end
-
end
-
-
def show
-
authorize!
-
end
-
-
def update
-
authorize!
-
-
if @district.update(district_params)
-
render :show, status: :ok
-
else
-
render json: @district.errors, status: :unprocessable_entity
-
end
-
end
-
-
def destroy
-
authorize!
-
-
@district.public_send(@district.discarded? ? :undiscard : :discard)
-
end
-
-
private
-
-
def discarded_scope
-
return :all unless params.key?(:a)
-
-
params[:a] == '1' ? :kept : :discarded
-
end
-
-
def district_params
-
params.fetch(:district, {})
-
.permit(:name, :school_ids)
-
end
-
-
def set_resources
-
@resources = District.public_send(discarded_scope)
-
@resources = @resources.search(params[:q]) if params[:q].present?
-
end
-
-
def set_district
-
@district =
-
District.friendly.find(params[:id])
-
end
-
end
-
# frozen_string_literal: true
-
-
class ProfilesController < ApplicationController
-
def show
-
authorize! current_user, with: ProfilePolicy
-
end
-
-
def update
-
authorize! current_user, with: ProfilePolicy
-
-
if current_user.update(profile_params)
-
render :show, status: :ok
-
else
-
render json: current_user.errors, status: :unprocessable_entity
-
end
-
end
-
-
private
-
-
def profile_params
-
params.fetch(:profile, {})
-
.permit(:email, :password, :password_confirmation)
-
end
-
end
-
# frozen_string_literal: true
-
-
class RepresentativesController < ApplicationController
-
before_action :set_resources, only: %i[index]
-
before_action :set_representative, only: %i[show update destroy]
-
-
def index
-
authorize!
-
-
@pagy, @representatives = pagy(@resources)
-
end
-
-
def create
-
@representative = Representative.new(representative_params)
-
-
authorize!
-
-
if @representative.save
-
render :show, status: :created
-
else
-
render json: @representative.errors, status: :unprocessable_entity
-
end
-
end
-
-
def show
-
authorize!
-
end
-
-
def update
-
authorize!
-
-
if @representative.update(representative_params)
-
render :show, status: :ok
-
else
-
render json: @representative.errors, status: :unprocessable_entity
-
end
-
end
-
-
def destroy
-
authorize!
-
-
@representative.public_send(@representative.discarded? ? :undiscard : :discard)
-
end
-
-
private
-
-
# TODO: https://actionpolicy.evilmartians.io/#/scoping
-
def discarded_scope
-
return :all unless params.key?(:a)
-
-
params[:a] == '1' ? :kept : :discarded
-
end
-
-
def representative_params
-
params.fetch(:representative, {}).permit(:title,
-
:first_name, :last_name, :email, :phone_number, :extra_text,
-
:image, image_data: [:id, :storage, metadata: %i[blurhash filename height mime_type size width]],
-
school_representatives_attributes: %i[id school_id _destroy])
-
end
-
-
def set_resources
-
@resources = Representative.public_send(discarded_scope)
-
@resources = @resources.search(params[:q]) if params[:q].present?
-
end
-
-
def set_representative
-
@representative =
-
Representative.friendly.find(params[:id])
-
end
-
end
-
# frozen_string_literal: true
-
-
class SchoolsController < ApplicationController
-
before_action :set_resources, only: %i[index]
-
before_action :set_school, only: %i[show update destroy]
-
-
def index
-
authorize!
-
-
@pagy, @schools = pagy(@resources)
-
end
-
-
def create
-
@school = School.new(school_params)
-
-
authorize!
-
-
if @school.save
-
render :show, status: :created
-
else
-
render json: @school.errors, status: :unprocessable_entity
-
end
-
end
-
-
def show
-
authorize!
-
end
-
-
def update
-
authorize!
-
-
if @school.update(school_params)
-
render :show, status: :ok
-
else
-
render json: @school.errors, status: :unprocessable_entity
-
end
-
end
-
-
def destroy
-
authorize!
-
-
@school.public_send(@school.discarded? ? :undiscard : :discard)
-
end
-
-
private
-
-
# TODO: https://actionpolicy.evilmartians.io/#/scoping
-
def discarded_scope
-
return :all unless params.key?(:a)
-
-
params[:a] == '1' ? :kept : :discarded
-
end
-
-
def school_params
-
params.fetch(:school, {}).permit(:name,
-
:district_id, :spots_id, :download_service_url, :banner, :logo,
-
banner_data: [:id, :storage, metadata: %i[blurhash filename height mime_type size width]],
-
logo_data: [:id, :storage, metadata: %i[blurhash filename height mime_type size width]],
-
school_representatives_attributes: %i[id representative_id _destroy],
-
user_schools_attributes: %i[id user_id _destroy])
-
end
-
-
def set_resources
-
@resources = School.includes(:district, :spots).public_send(discarded_scope)
-
@resources = @resources.search(params[:q]) if params[:q].present?
-
end
-
-
def set_school
-
@school =
-
School.friendly.find(params[:id])
-
end
-
end
-
class SearchesController < ApplicationController
-
def show
-
@results =
-
SPOTS::Customer.where(['CustomerID LIKE ?', "%#{params[:q]}%"])
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::CustomersController < SPOTSController
-
def show
-
@customer =
-
SPOTS::Customer.find(params[:id])
-
-
authorize! @customer
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::ImagesController < SPOTSController
-
before_action :set_image
-
-
def show
-
authorize! @image
-
-
send_data @image.PictureData, disposition: :attachment,
-
filename: @image.filename
-
end
-
-
private
-
-
def set_image
-
@image =
-
SPOTS::ImagePropertiesPicture.find(image_params)
-
end
-
-
def image_params
-
[params[:customer_id], params[:session_id], params[:id]]
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::Reports::PhotographedController < SPOTS::ReportsController
-
def index
-
authorize! with: SPOTS::Reports::PhotographedPolicy
-
-
@pagy, @customers =
-
pagy(@school.spots.customers.public_send(params.fetch(:a, '1') == '1' ? :photographed : :not_photographed))
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::Reports::UpcomingAppointmentsController < SPOTS::ReportsController
-
def index
-
authorize! with: SPOTS::Reports::UpcomingAppointmentsPolicy
-
-
@pagy, @appointments = pagy_array(@school.spots.upcoming_appointments)
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::Reports::YearbookPosesController < SPOTS::ReportsController
-
before_action :ensure_pose_chosen_report_enabled
-
-
def index
-
authorize! with: SPOTS::Reports::YearbookPosesPolicy
-
-
@pagy, @customers =
-
pagy(@school.spots.customers.public_send(params.fetch(:a, '1') == '1' ? :with_yearbook_pose : :without_yearbook_pose))
-
end
-
-
private
-
-
def ensure_pose_chosen_report_enabled
-
return if Configurable.pose_chosen_report_enabled?
-
-
render json: {
-
error: 'Report unavailable'
-
}, status: :forbidden
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::ReportsController < SPOTSController
-
before_action :set_school
-
before_action :require_connected_spots_school
-
-
authorize :school, through: :set_school
-
-
private
-
-
def require_connected_spots_school
-
return if @school.spots.present?
-
-
# TODO: Create constant
-
render json: {
-
error: 'Portrait Data Unavailable'
-
}, status: :bad_request
-
end
-
-
def set_school
-
@school = School.find(params[:school_id])
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTSController < ApplicationController
-
# TODO: db switching for "simple feature"
-
# around_action :connect_to_desired_spots_database
-
-
rescue_from TinyTds::Error do |ex|
-
response.headers['x-exception-message'] = ex.message unless Rails.env.production?
-
-
# TODO: Create constant
-
render json: {
-
error: 'Portrait Data Connection Error'
-
}, status: :service_unavailable
-
end
-
-
private
-
-
# NOTE: Use only for reference. Use session or other param to define index
-
# used for new/original_database. Allows for per request calls. Investigate
-
# potential latency issues. Investigate, if happening, impact of new connections.
-
def connect_to_desired_spots_database
-
# rubocop:disable Style/BracesAroundHashParameters
-
SPOTSRecord.establish_connection({
-
adapter: :sqlserver,
-
database: Configurable.spots_db_name,
-
host: new_database,
-
username: Configurable.spots_db_user,
-
password: Configurable.spots_db_pass
-
})
-
# rubocop:enable Style/BracesAroundHashParameters
-
-
yield
-
-
# rubocop:disable Style/BracesAroundHashParameters
-
SPOTSRecord.establish_connection({
-
adapter: :sqlserver,
-
database: Configurable.spots_db_name,
-
host: original_database,
-
username: Configurable.spots_db_user,
-
password: Configurable.spots_db_pass
-
})
-
# rubocop:enable Style/BracesAroundHashParameters
-
end
-
end
-
# frozen_string_literal: true
-
-
class UploadsController < ApplicationController
-
before_action :ensure_single_source, only: %i[index create]
-
before_action :set_source, only: %i[index create]
-
-
before_action :set_resources, only: %i[index]
-
before_action :set_upload, only: %i[show update destroy]
-
-
authorize :source, through: :set_source
-
-
def index
-
authorize!
-
-
@pagy, @uploads = pagy(@resources)
-
end
-
-
def create
-
authorize!
-
-
@upload =
-
@source.uploads.new(upload_params.merge(created_ip: request.remote_ip, updated_ip: request.remote_ip, user_id: current_user.id))
-
-
if @upload.save
-
render :show, status: :created
-
else
-
render json: @upload.errors, status: :unprocessable_entity
-
end
-
end
-
-
def show
-
authorize!
-
end
-
-
def update
-
authorize!
-
-
if @upload.update(upload_params.merge(updated_ip: request.remote_ip, user_id: current_user.id))
-
render :show, status: :ok
-
else
-
render json: @upload.errors, status: :unprocessable_entity
-
end
-
end
-
-
def destroy
-
authorize!
-
-
@upload.discarded_ip = request.remote_ip
-
-
@upload.public_send(@upload.discarded? ? :undiscard : :discard)
-
end
-
-
private
-
-
# TODO: https://actionpolicy.evilmartians.io/#/scoping
-
def discarded_scope
-
return :all unless params.key?(:a)
-
-
params[:a] == '1' ? :kept : :discarded
-
end
-
-
def ensure_single_source
-
return unless params.slice(:user_id, :school_id).keys.size > 1
-
-
head :bad_request
-
end
-
-
def set_resources
-
@resources =
-
if @source == Upload
-
@source
-
else
-
@source.uploads
-
end.includes(:user, :school).public_send(discarded_scope)
-
@resources = @resources.search(params[:q]) if params[:q].present?
-
end
-
-
def set_source
-
@source =
-
if params.key?(:school_id)
-
School.friendly.find(params[:school_id])
-
elsif params.key?(:user_id)
-
User.friendly.find(params[:user_id])
-
else
-
Upload
-
end
-
end
-
-
def set_upload
-
@upload =
-
Upload.find(params[:id])
-
end
-
-
def upload_params
-
params.fetch(:upload, {}).permit(:note,
-
data_data: [:id, :storage, metadata: %i[blurhash filename height mime_type size width]])
-
end
-
end
-
# frozen_string_literal: true
-
-
class UsersController < ApplicationController
-
before_action :set_resources, only: %i[index]
-
before_action :set_user, only: %i[show update destroy]
-
-
def index
-
authorize!
-
-
@pagy, @users = pagy(@resources)
-
end
-
-
def create
-
@user = User.new(user_params)
-
-
authorize!
-
-
@user.access_code = Configurable.sign_up_access_code
-
@user.activated_at = Time.current
-
-
if @user.save
-
render :show, status: :created
-
else
-
render json: @user.errors, status: :unprocessable_entity
-
end
-
end
-
-
def show
-
authorize!
-
end
-
-
def update
-
authorize!
-
-
if @user.update(user_params)
-
render :show, status: :ok
-
else
-
render json: @user.errors, status: :unprocessable_entity
-
end
-
end
-
-
def destroy
-
authorize!
-
-
@user.public_send(@user.discarded? ? :undiscard : :discard)
-
end
-
-
private
-
-
# TODO: https://actionpolicy.evilmartians.io/#/scoping
-
def discarded_scope
-
return :all unless params.key?(:a)
-
-
if params[:a] == '1'
-
:kept
-
else
-
params[:a] == '-1' ? :pending : :discarded
-
end
-
end
-
-
def user_params
-
permitted_params = [
-
:first_name, :last_name, :email, :password, :password_confirmation,
-
:avatar, avatar_data: [:id, :storage, metadata: %i[blurhash filename height mime_type size width]]
-
]
-
-
if (params.dig(:user, :type) == 'Administrator' && current_user.is_a?(Administrator)) ||
-
(params.dig(:user, :type) == 'StudioUser' && (current_user.is_a?(Administrator) || current_user.is_a?(StudioUser)))
-
permitted_params += %w[type]
-
end
-
-
if (params.dig(:user, :type) || @user&.type) == 'SchoolUser'
-
permitted_params += [user_schools_attributes: %i[id school_id _destroy]]
-
end
-
-
params.fetch(:user, {}).permit(permitted_params)
-
end
-
-
def set_resources
-
@resources = User.where.not(id: current_user.id).public_send(discarded_scope)
-
@resources = @resources.search(params[:q]) if params[:q].present?
-
end
-
-
def set_user
-
@user =
-
User.friendly.find(params[:id])
-
end
-
end
-
# frozen_string_literal: true
-
-
class ApplicationDecorator < Draper::Decorator
-
# Define methods for all decorated objects.
-
# Helpers are accessed through `helpers` (aka `h`). For example:
-
#
-
# def percent_amount
-
# h.number_to_percentage object.amount, precision: 2
-
# end
-
end
-
# frozen_string_literal: true
-
-
class ApplicationJob < ActiveJob::Base
-
retry_on Exception, attempts: :unlimited
-
-
# Most jobs are safe to ignore if the underlying records are no longer available
-
discard_on ActiveJob::DeserializationError
-
end
-
class ShrineDestroyJob < ApplicationJob
-
queue_as :attachment_processing
-
-
def perform(attacher_class, data)
-
attacher_class = Object.const_get(attacher_class)
-
-
attacher = attacher_class.from_data(data)
-
attacher.destroy
-
end
-
end
-
class ShrinePromoteJob < ApplicationJob
-
queue_as :attachment_processing
-
-
def perform(attacher_class, record_class, record_id, name, file_data)
-
attacher_class = Object.const_get(attacher_class)
-
record = Object.const_get(record_class).find(record_id) # if using Active Record
-
-
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
-
attacher.atomic_promote
-
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
-
# attachment has changed or record has been deleted, nothing to do
-
end
-
end
-
# frozen_string_literal: true
-
-
class ApplicationMailer < ActionMailer::Base
-
default from: Configurable.email_customer_service
-
-
layout 'mailer'
-
end
-
class Administrator < User
-
attr_readonly :activated_at
-
-
before_create :set_activated_at
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: TRACKED_PARAMETERS
-
end
-
# frozen_string_literal: true
-
-
class ApplicationRecord < ActiveRecord::Base
-
primary_abstract_class
-
-
self.implicit_order_column = :created_at
-
end
-
# frozen_string_literal: true
-
-
class District < ApplicationRecord
-
include Discard::Model
-
include FriendlyId
-
include PgSearch::Model
-
include PublicActivity::Model
-
-
has_many :schools, inverse_of: :district,
-
dependent: :destroy
-
-
validates :name,
-
presence: true,
-
uniqueness: { case_sensitive: false, scope: %i[discarded_at] }
-
-
after_validation :move_friendly_id_error_to_name
-
-
after_discard { schools.kept.update_all(discarded_at: discarded_at) }
-
-
friendly_id :name, use: %i[slugged]
-
-
has_paper_trail on: %i[create destroy update]
-
-
pg_search_scope :search,
-
against: {
-
name: 'A'
-
},
-
using: {
-
tsearch: {
-
prefix: true,
-
negation: true,
-
highlight: true,
-
dictionary: :english,
-
tsvector_column: :name_tsearch_tsvector
-
},
-
dmetaphone: {
-
tsvector_column: :name_dmetaphone_tsvector
-
}
-
}
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: {
-
name: :name,
-
school_count: -> (_, model) { model.schools.size },
-
discarded: :discarded?,
-
discarded_at: :discarded_at
-
}
-
-
private
-
-
def move_friendly_id_error_to_name
-
errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
-
end
-
-
def should_generate_new_friendly_id?
-
name_changed? || super
-
end
-
end
-
# frozen_string_literal: true
-
-
class Representative < ApplicationRecord
-
include Discard::Model
-
include FriendlyId
-
include PgSearch::Model
-
include PublicActivity::Model
-
include SquareImageUploader::Attachment(:avatar)
-
-
has_many :school_representatives, inverse_of: :representative,
-
dependent: :destroy
-
has_many :schools,
-
through: :school_representatives
-
-
validates :first_name,
-
presence: true
-
-
validates :last_name,
-
presence: true
-
-
after_validation :move_friendly_id_error_to_name
-
-
after_discard { school_representatives.destroy_all }
-
-
accepts_nested_attributes_for :school_representatives,
-
allow_destroy: true, reject_if: :all_blank
-
-
friendly_id :full_name, use: %i[slugged]
-
-
has_paper_trail on: %i[create destroy update]
-
-
pg_search_scope :search,
-
against: {
-
first_name: 'B',
-
last_name: 'A'
-
},
-
using: {
-
tsearch: {
-
prefix: true,
-
negation: true,
-
highlight: true,
-
dictionary: :english,
-
tsvector_column:
-
%i[last_name_tsearch_tsvector first_name_tsearch_tsvector]
-
},
-
dmetaphone: {
-
tsvector_column:
-
%i[last_name_dmetaphone_tsvector first_name_dmetaphone_tsvector]
-
}
-
}
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: {
-
title: :title,
-
first_name: :first_name,
-
last_name: :last_name,
-
email: :email,
-
phone_number: :phone_number,
-
extra_text: :extra_text,
-
avatar_changed: :avatar_data_changed?,
-
schools_count: -> (_, model) { model.schools.size },
-
discarded: :discarded?,
-
discarded_at: :discarded_at
-
}
-
-
def full_name(with_title: false)
-
f_n = "#{first_name} #{last_name}"
-
-
return f_n unless with_title
-
-
"#{title} #{f_n}"
-
end
-
-
private
-
-
def move_friendly_id_error_to_name
-
return unless errors[:friendly_id].present?
-
-
errors.delete(:friendly_id)
-
-
errors.add :base, 'Full name combination is reserved'
-
end
-
-
def should_generate_new_friendly_id?
-
first_name_changed? || last_name_changed? || super
-
end
-
end
-
class School < ApplicationRecord
-
include Discard::Model
-
include FriendlyId
-
include PgSearch::Model
-
include PublicActivity::Model
-
include SquareImageUploader::Attachment(:logo)
-
include WideImageUploader::Attachment(:banner)
-
-
scope :with_spots, -> { where.not(spots_id: nil) }
-
-
belongs_to :district, inverse_of: :schools,
-
touch: true
-
belongs_to :spots, class_name: 'SPOTS::School',
-
inverse_of: :portal, optional: true
-
-
has_many :customers,
-
through: :spots, disable_joins: true
-
-
has_many :school_representatives, inverse_of: :school,
-
dependent: :destroy
-
has_many :representatives,
-
through: :school_representatives
-
-
has_many :uploads, inverse_of: :school,
-
dependent: :nullify
-
-
has_many :user_schools, inverse_of: :school,
-
dependent: :destroy
-
has_many :users,
-
through: :user_schools
-
-
validates :name,
-
presence: true,
-
uniqueness: { case_sensitive: false, scope: %i[discarded_at] }
-
-
# REVIEW: Can this be replaced by validates length on scope
-
validate :selected_district_is_active
-
-
after_validation :move_friendly_id_error_to_name
-
after_validation :move_district_error_to_district_id
-
-
after_discard { school_representatives.destroy_all }
-
after_discard { user_schools.destroy_all }
-
-
accepts_nested_attributes_for :school_representatives,
-
allow_destroy: true, reject_if: :all_blank
-
-
accepts_nested_attributes_for :user_schools,
-
allow_destroy: true, reject_if: :all_blank
-
-
delegate :name,
-
to: :district, prefix: true
-
delegate :statistics,
-
to: :spots, prefix: true, allow_nil: true
-
-
friendly_id :name, use: %i[slugged]
-
-
has_paper_trail on: %i[create destroy update]
-
-
pg_search_scope :search,
-
against: {
-
name: 'A'
-
},
-
using: {
-
tsearch: {
-
prefix: true,
-
negation: true,
-
highlight: true,
-
dictionary: :english,
-
tsvector_column: :name_tsearch_tsvector
-
},
-
dmetaphone: {
-
tsvector_column: :name_dmetaphone_tsvector
-
}
-
}
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: {
-
name: :last_name,
-
spots_id: :spots_id,
-
district_name: :district_name,
-
logo_changed: :logo_data_changed?,
-
banner_changed: :banner_data_changed?,
-
discarded: :discarded?,
-
discarded_at: :discarded_at
-
}
-
-
private
-
-
def move_district_error_to_district_id
-
errors.add :district_id, *errors.delete(:district) if errors[:district].present?
-
end
-
-
def move_friendly_id_error_to_name
-
errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
-
end
-
-
def selected_district_is_active
-
return unless district
-
-
errors.add :district_id, 'must be active' if district_changed? && district.discarded?
-
end
-
-
def should_generate_new_friendly_id?
-
name_changed? || super
-
end
-
end
-
class SchoolRepresentative < ApplicationRecord
-
self.primary_key = %i[school_id representative_id]
-
-
belongs_to :school, inverse_of: :school_representatives,
-
touch: true
-
belongs_to :representative, inverse_of: :school_representatives,
-
touch: true
-
-
validates :school_id,
-
uniqueness: { scope: %i[representative_id] }
-
-
validates :representative_id,
-
uniqueness: { scope: %i[school_id] }
-
-
validate :only_active_associated_allowed,
-
on: %i[create update]
-
-
delegate :discarded?, :kept?,
-
to: :representative, prefix: true
-
-
delegate :discarded?, :kept?,
-
to: :school, prefix: true
-
-
private
-
-
def only_active_associated_allowed
-
errors.add :school_id, 'must be active' if school_discarded?
-
errors.add :representative_id, 'must be active' if representative_discarded?
-
end
-
end
-
class SchoolUser < User
-
attr_accessor :terms_of_service
-
attr_accessor :access_code
-
-
has_many :user_schools, inverse_of: :user,
-
foreign_key: :user_id, dependent: :destroy
-
has_many :schools,
-
through: :user_schools
-
-
validates :access_code,
-
comparison: {
-
equal_to: Configurable.sign_up_access_code,
-
message: 'does not match, please try again.'
-
}, on: %i[create]
-
validates :terms_of_service,
-
acceptance: true, on: %i[create]
-
validates :user_schools,
-
length: {
-
minimum: 1,
-
message: 'must belong to at least one school'
-
}
-
-
accepts_nested_attributes_for :user_schools, allow_destroy: true, reject_if: :all_blank
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: TRACKED_PARAMETERS.merge(schools_count: -> (_, model) { model.schools.size })
-
-
def active_for_authentication?
-
super && schools.any?
-
end
-
end
-
# frozen_string_literal: true
-
-
module SPOTS
-
end
-
# frozen_string_literal: true
-
-
module SPOTS
-
class Customer < SPOTSRecord
-
# REVIEW: if this works as intended
-
OUTER_JOIN_POSE_ORDERS_SQL = <<~SQL
-
LEFT OUTER JOIN [PoseOrders]
-
ON [PoseOrders].[CustomerID] = [Customers].[CustomerID]
-
SQL
-
-
scope :photographed, -> { joins(:recent_session).includes(:recent_session).where(Photographed: true) }
-
scope :not_photographed, -> { where(Photographed: [false, nil, '']) }
-
-
scope :with_poses, -> { photographed.joins(:pose_orders).distinct }
-
scope :without_poses, lambda {
-
photographed.joins(OUTER_JOIN_POSE_ORDERS_SQL)
-
.where('[PoseOrders].[CustomerID] IS NULL')
-
.distinct
-
} # REVIEW: if this works as intended
-
-
scope :with_yearbook_pose, lambda {
-
includes(:yearbook_poses)
-
.with_poses.merge(SPOTS::PoseOrder.yearbook)
-
}
-
scope :without_yearbook_pose, lambda {
-
photographed
-
.where.not(CustomerID: SPOTS::PoseOrder.yearbook.select(:CustomerID))
-
.distinct
-
}
-
-
self.primary_key = 'CustomerID'
-
-
belongs_to :school,
-
class_name: 'SPOTS::School',
-
foreign_key: 'SchoolID', inverse_of: :customers
-
-
has_many :images,
-
through: :pose_orders, inverse_of: :customer
-
# REVIEW: through??
-
has_many :yearbook_poses, -> { SPOTS::PoseOrder.yearbook },
-
class_name: 'SPOTS::PoseOrder', foreign_key: 'CustomerID',
-
inverse_of: :customer, dependent: :restrict_with_exception
-
has_many :pose_orders,
-
class_name: 'SPOTS::PoseOrder', foreign_key: 'CustomerID',
-
inverse_of: :customer, dependent: :restrict_with_exception
-
-
has_one :recent_session, -> { SPOTS::Session.most_recent },
-
class_name: 'SPOTS::Session', foreign_key: 'CustomerID',
-
inverse_of: :customer, dependent: :restrict_with_exception
-
has_many :sessions,
-
class_name: 'SPOTS::Session', foreign_key: 'CustomerID',
-
inverse_of: :customer, dependent: :restrict_with_exception
-
-
validates :FirstName, :LastName, :StudentID, :EMail,
-
:Address, :City, :State, :ZipCode, :Phone,
-
presence: true
-
-
before_destroy -> { Rails::VERSION::MAJOR >= 5 ? throw(:abort) : false },
-
prepend: true
-
-
def full_name
-
"#{self.FirstName} #{self.LastName}"
-
end
-
-
# rubocop:disable Naming/MethodName
-
def Photographed
-
super || false
-
end
-
# rubocop:enable Naming/MethodName
-
-
# TODO: presenter
-
# REVIEW: should move to SPOTS::PoseOrder.ybpn?
-
def yearbook_pose_numbers
-
yearbook_poses.map(&:pose_number)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
module SPOTS
-
class ImagePropertiesPicture < SPOTSRecord
-
self.table_name = 'ImagePropertiesPicture'
-
self.primary_keys = :CustomerID, :SessionNumber, :Pose
-
-
belongs_to :customer,
-
foreign_key: :CustomerID, inverse_of: :images
-
belongs_to :pose_order,
-
foreign_key: %i[CustomerID Pose SessionNumber], inverse_of: :image
-
has_one :school, through: :customer
-
-
def height(max = nil)
-
return dimensions[1] if max.blank?
-
-
height < width && (max * ratio) || max
-
end
-
-
def ratio
-
height > width ? width / height : height / width
-
end
-
-
def to_io
-
return @io if defined? @io
-
-
@io = StringIO.new
-
@io.set_encoding Encoding::BINARY
-
@io << self.PictureData
-
end
-
-
def width(max = nil)
-
return dimensions[0] if max.blank?
-
-
height > width && (max * ratio) || max
-
end
-
-
def filename
-
"#{self.CustomerID}_#{self.SessionNumber}_#{self.Pose}.jpg"
-
end
-
-
private
-
-
def dimensions
-
@dimensions ||= FastImage.size(to_io).map { |dim| BigDecimal(dim) }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
module SPOTS
-
class PoseOrder < SPOTSRecord
-
scope :yearbook, -> { where(YB: true).includes(:image) }
-
scope :not_yearbook, -> { where.not(YB: true) }
-
-
self.table_name = 'PoseOrders'
-
self.primary_keys = :CustomerID, :PoseNo, :SessionNumber, :OrderNumber
-
-
# REVIEW: order matters?
-
has_one :image,
-
class_name: 'SPOTS::ImagePropertiesPicture',
-
foreign_key: %i[CustomerID Pose SessionNumber],
-
inverse_of: :pose_order, dependent: :restrict_with_exception
-
-
belongs_to :customer,
-
class_name: 'SPOTS::Customer',
-
foreign_key: 'CustomerID', inverse_of: :pose_orders
-
-
# TODO: presenter
-
def pose_number
-
"#{self.SessionNumber}#{self.PoseNo}"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
module SPOTS
-
class School < SPOTSRecord
-
self.primary_key = 'SchoolID'
-
-
has_one :portal,
-
class_name: '::School', foreign_key: :spots_id,
-
inverse_of: :spots, dependent: :restrict_with_exception
-
-
has_many :customers,
-
class_name: 'SPOTS::Customer', foreign_key: 'SchoolID',
-
inverse_of: :school, dependent: :restrict_with_exception
-
-
def statistics
-
return @statistics if defined?(@statistics)
-
-
data = customers.group(:Photographed).size
-
appointment_count = upcoming_appointments.count
-
-
@statistics = { photographed: data.delete(true), upcoming: appointment_count }
-
@statistics[:not_photographed] = data.values.sum - appointment_count
-
-
@statistics
-
end
-
-
def upcoming_appointments
-
sql = <<~SQL
-
SELECT DISTINCT
-
UPPER(dbo.Timetrade_Data.TimetradeLastName) AS LastName,
-
UPPER(dbo.Timetrade_Data.TimetradeFirstName) AS FirstName,
-
Format(dbo.Timetrade_Data.ScheduleStartTime, 'MM-dd-yyyy hh:mm tt') AS AppointmentAt,
-
dbo.Timetrade_Data.StudentIDNumber AS StudentID,
-
dbo.Timetrade_Data.CustomerID,
-
dbo.Timetrade_Data.ConfirmationNumber AS ConfirmationNumber,
-
dbo.SchoolID_SchoolIDAlpha.SchoolID AS SPOTSSchoolNumber
-
FROM
-
dbo.Timetrade_Data INNER JOIN dbo.SchoolID_SchoolIDAlpha ON dbo.Timetrade_Data.SchoolID = dbo.SchoolID_SchoolIDAlpha.SchoolIDAlpha
-
WHERE
-
((dbo.Timetrade_Data.EventName = 'appointmentBooked') OR (dbo.Timetrade_Data.EventName = 'appointmentChanged')) AND
-
(dbo.Timetrade_Data.CancelledAt IS NULL) AND
-
(dbo.Timetrade_Data.ScheduleStartTime > GETDATE()) AND
-
dbo.SchoolID_SchoolIDAlpha.SchoolID = @0
-
ORDER BY
-
LastName, FirstName
-
SQL
-
-
SPOTS::Customer.find_by_sql(sql,
-
[ActiveRecord::Relation::QueryAttribute.new(nil, self.SchoolID.to_s, ActiveRecord::Type::Integer.new)])
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
module SPOTS
-
class Session < SPOTSRecord
-
scope :most_recent, -> { order(DateTime: :desc).limit(1) }
-
-
self.primary_key = 'SessionID'
-
-
belongs_to :customer,
-
class_name: 'SPOTS::Customer',
-
foreign_key: 'CustomerID', inverse_of: :sessions
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTSRecord < ActiveRecord::Base
-
self.abstract_class = true
-
-
# rubocop:disable Style/BracesAroundHashParameters
-
establish_connection({
-
adapter: :sqlserver,
-
database: Configurable.spots_db_name,
-
host: Configurable.spots_db_host,
-
username: Configurable.spots_db_user,
-
password: Configurable.spots_db_pass
-
})
-
# rubocop:enable Style/BracesAroundHashParameters
-
end
-
class StudioUser < User
-
attr_readonly :activated_at
-
-
before_create :set_activated_at
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: TRACKED_PARAMETERS
-
end
-
# frozen_string_literal: true
-
-
class Upload < ApplicationRecord
-
include DataUploader::Attachment(:data)
-
include Discard::Model
-
include PgSearch::Model
-
include PublicActivity::Model
-
-
belongs_to :user, touch: true
-
belongs_to :school, touch: true
-
-
validates :data,
-
presence: true
-
-
has_paper_trail on: %i[create destroy update]
-
-
pg_search_scope :search,
-
against: {
-
note: 'A'
-
},
-
using: {
-
tsearch: {
-
prefix: true,
-
negation: true,
-
highlight: true,
-
dictionary: :english,
-
tsvector_column: :note_tsearch_tsvector
-
},
-
dmetaphone: {
-
tsvector_column: :note_dmetaphone_tsvector
-
}
-
}
-
-
tracked owner: -> (controller, _) { controller&.true_user },
-
params: {
-
note: :note,
-
discarded: :discarded?,
-
discarded_at: :discarded_at
-
}
-
end
-
# frozen_string_literal: true
-
-
class User < ApplicationRecord
-
include Discard::Model
-
include FriendlyId
-
include PgSearch::Model
-
include PublicActivity::Model
-
include SquareImageUploader::Attachment(:avatar)
-
-
TRACKED_PARAMETERS = {
-
first_name: :first_name,
-
last_name: :last_name,
-
activated: :activated?,
-
discarded: :discarded?,
-
discarded_at: :discarded_at,
-
password_changed: :encrypted_password_changed?
-
}.freeze
-
-
scope :pending, -> { kept.where(activated_at: nil) }
-
-
has_many :uploads, inverse_of: :user,
-
dependent: :nullify
-
-
validates :first_name,
-
presence: true
-
validates :last_name,
-
presence: true
-
validates :password_confirmation,
-
presence: true, if: :encrypted_password_changed?
-
-
devise :database_authenticatable, :registerable,
-
:recoverable, :rememberable, :trackable,
-
:validatable, :zxcvbnable
-
-
friendly_id :full_name, use: %i[slugged]
-
-
has_paper_trail on: %i[create destroy update]
-
-
pg_search_scope :search,
-
against: {
-
first_name: 'B',
-
last_name: 'A'
-
},
-
using: {
-
tsearch: {
-
prefix: true,
-
negation: true,
-
highlight: true,
-
dictionary: :english,
-
tsvector_column:
-
%i[last_name_tsearch_tsvector first_name_tsearch_tsvector]
-
},
-
dmetaphone: {
-
tsvector_column:
-
%i[last_name_dmetaphone_tsvector first_name_dmetaphone_tsvector]
-
}
-
}
-
-
def active_for_authentication?
-
super && !discarded? && activated?
-
end
-
-
def activated?
-
activated_at.present?
-
end
-
-
def full_name
-
"#{first_name} #{last_name}"
-
end
-
-
private
-
-
def set_activated_at
-
self.activated_at = Time.current
-
end
-
end
-
class UserSchool < ApplicationRecord
-
self.primary_key = %i[user_id school_id]
-
-
belongs_to :user, inverse_of: :user_schools,
-
class_name: 'SchoolUser', touch: true
-
belongs_to :school, inverse_of: :user_schools,
-
touch: true
-
-
validates :user_id,
-
uniqueness: { scope: %i[school_id] }
-
-
validates :school_id,
-
uniqueness: { scope: %i[user_id] }
-
-
validate :only_active_associated_allowed,
-
on: %i[create update]
-
-
delegate :activated?, :discarded?, :kept?,
-
to: :user, prefix: true
-
-
delegate :discarded?, :kept?,
-
to: :school, prefix: true
-
-
private
-
-
def only_active_associated_allowed
-
errors.add :user_id, 'must be active' if user_discarded?
-
errors.add :school_id, 'must be active' if school_discarded?
-
end
-
end
-
# frozen_string_literal: true
-
-
class ActivityPolicy < ApplicationPolicy
-
def index?
-
administrative_user?
-
end
-
end
-
# frozen_string_literal: true
-
-
class ApplicationPolicy < ActionPolicy::Base
-
private
-
-
def administrative_user?
-
studio_user? || administrator?
-
end
-
-
def administrator?
-
user.is_a?(Administrator)
-
end
-
-
def owner?
-
record.user_id == user.id
-
end
-
-
def studio_user?
-
user.is_a?(StudioUser)
-
end
-
end
-
# frozen_string_literal: true
-
-
class ConfigurablePolicy < ApplicationPolicy
-
def show?
-
administrator?
-
end
-
-
def update?
-
administrator?
-
end
-
end
-
# frozen_string_literal: true
-
-
class DistrictPolicy < ApplicationPolicy
-
def index?
-
administrative_user?
-
end
-
-
def create?
-
administrative_user?
-
end
-
-
def show?
-
administrative_user?
-
end
-
-
def update?
-
administrative_user?
-
end
-
-
def destroy?
-
administrative_user?
-
end
-
end
-
class ProfilePolicy < ApplicationPolicy
-
def show?
-
record.id == user.id
-
end
-
-
def update?
-
record.id == user.id
-
end
-
end
-
# frozen_string_literal: true
-
-
class RepresentativePolicy < ApplicationPolicy
-
def index?
-
administrative_user?
-
end
-
-
def create?
-
administrative_user?
-
end
-
-
def show?
-
administrative_user?
-
end
-
-
def update?
-
administrative_user?
-
end
-
-
def destroy?
-
administrative_user?
-
end
-
end
-
# frozen_string_literal: true
-
-
class SchoolPolicy < ApplicationPolicy
-
def index?
-
administrative_user?
-
end
-
-
def create?
-
administrative_user?
-
end
-
-
def show?
-
administrative_user?
-
end
-
-
def update?
-
administrative_user?
-
end
-
-
def destroy?
-
administrative_user?
-
end
-
end
-
class SPOTS::CustomerPolicy < SPOTSPolicy
-
def show?
-
administrative_user?
-
end
-
end
-
# frozen_string_literal: true
-
-
class SPOTS::ImagePropertiesPicturePolicy < SPOTSPolicy
-
def show?
-
allow! if administrative_user?
-
-
user.schools.exists?(spots_id: record.school.SchoolID)
-
end
-
end
-
class SPOTS::Reports::PhotographedPolicy < SPOTS::ReportsPolicy
-
def index?
-
allow! if administrative_user?
-
-
user.is_a?(SchoolUser) && user.schools.exists?(school.id)
-
end
-
end
-
class SPOTS::Reports::UpcomingAppointmentsPolicy < ApplicationPolicy
-
def index?
-
allow! if administrative_user?
-
-
user.is_a?(SchoolUser) && user.schools.exists?(school.id)
-
end
-
end
-
class SPOTS::Reports::YearbookPosesPolicy < SPOTS::ReportsPolicy
-
def index?
-
allow! if administrative_user?
-
-
user.is_a?(SchoolUser) && user.schools.exists?(school.id)
-
end
-
end
-
class SPOTS::ReportsPolicy < SPOTSPolicy
-
authorize :school
-
end
-
class SPOTSPolicy < ApplicationPolicy
-
end
-
class UploadPolicy < ApplicationPolicy
-
authorize :source
-
-
def index?
-
allow! if administrative_user?
-
allow! if source.is_a?(School) && user.schools.exists?(source.id)
-
-
source.is_a?(SchoolUser) && source.id == user.id
-
end
-
-
def create?
-
allow! if administrative_user?
-
-
source.is_a?(School) && user.schools.exists?(source.id)
-
end
-
-
def show?
-
administrative_user? || owner?
-
end
-
-
def update?
-
administrative_user? || owner?
-
end
-
-
def destroy?
-
administrative_user? || owner?
-
end
-
end
-
# frozen_string_literal: true
-
-
class UserPolicy < ApplicationPolicy
-
def index?
-
administrative_user?
-
end
-
-
def create?
-
allow! if administrator?
-
deny! unless administrative_user?
-
-
!record.is_a?(Administrator)
-
end
-
-
def show?
-
administrative_user? || record.id == user.id
-
end
-
-
def update?
-
allow! if administrator? || record.id == user.id
-
deny! unless administrative_user?
-
-
!record.is_a?(Administrator)
-
end
-
-
def destroy?
-
allow! if administrator?
-
deny! unless administrative_user?
-
-
!record.is_a?(Administrator)
-
end
-
end
-
# frozen_string_literal: true
-
-
class DataUploader < Shrine
-
end
-
# frozen_string_literal: true
-
-
class SquareImageUploader < Shrine
-
end
-
# frozen_string_literal: true
-
-
class WideImageUploader < Shrine
-
end