diff --git a/.gitignore b/.gitignore index e9479f79c..2609e75c8 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ node_modules *.local .vite +# Ignore local Active Storage +storage diff --git a/Gemfile b/Gemfile index f06e485ee..a1ce607b1 100644 --- a/Gemfile +++ b/Gemfile @@ -46,6 +46,8 @@ gem "country_select" gem "turbo-rails", "~> 2.0" gem "stimulus-rails", "~> 1.3" +gem "active_storage_validations", "~> 3.0" + group :development, :test do gem "better_errors" gem "brakeman", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 07542e00e..14ed44324 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,6 +47,12 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_storage_validations (3.0.2) + activejob (>= 6.1.4) + activemodel (>= 6.1.4) + activestorage (>= 6.1.4) + activesupport (>= 6.1.4) + marcel (>= 1.0.3) activejob (8.1.0.beta1) activesupport (= 8.1.0.beta1) globalid (>= 0.3.6) @@ -446,6 +452,7 @@ PLATFORMS ruby DEPENDENCIES + active_storage_validations (~> 3.0) apipie-rails (~> 1.5.0) aws-sdk-s3 bcrypt (= 3.1.16) diff --git a/app/decorators/workshop_decorator.rb b/app/decorators/workshop_decorator.rb index 994983a3a..36fda1ac2 100644 --- a/app/decorators/workshop_decorator.rb +++ b/app/decorators/workshop_decorator.rb @@ -148,18 +148,17 @@ def main_image end def thumbnail_image - if legacy && !thumbnail.exists? - main_image - else + # TODO Figure out if we need main_image + if thumbnail.attached? thumbnail end end def header_image - if !header.exists? - thumbnail_image - else + if header.attached? header + elsif thumbnail.attached? + thumbnail end end diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 34cf409cb..17a28f21a 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -1,10 +1,11 @@ class Attachment < ApplicationRecord belongs_to :owner, polymorphic: true - has_attached_file :file - validates_attachment :file, content_type: { content_type: %w(application/pdf application/msword image/gif image/jpg image/jpeg image/png) } + ACCEPTED_CONTENT_TYPES = %w[application/pdf application/msword image/gif image/jpeg image/png].freeze + has_one_attached :file + validates :file, content_type: ACCEPTED_CONTENT_TYPES def name - 'Pdf Attachment' + "Pdf Attachment" end end diff --git a/app/models/ckeditor/attachment_file.rb b/app/models/ckeditor/attachment_file.rb index 9b44defd4..b6c5daab3 100644 --- a/app/models/ckeditor/attachment_file.rb +++ b/app/models/ckeditor/attachment_file.rb @@ -1,11 +1,6 @@ class Ckeditor::AttachmentFile < ApplicationRecord # Ckeditor::Asset - # has_attached_file :data, - # :url => "/ckeditor_assets/attachments/:id/:filename", - # :path => "/ckeditor_assets/attachments/:id/:filename" - # - # validates_attachment_presence :data - # validates_attachment_size :data, :less_than => 100.megabytes - # do_not_validate_attachment_file_type :data + has_one_attached :data + validates :data, size: {less_than: 100.megabytes}, attached: true def url_thumb @url_thumb ||= nil # Ckeditor::Utils.filethumb(filename) diff --git a/app/models/ckeditor/picture.rb b/app/models/ckeditor/picture.rb index 94e486be0..d90c8d70c 100644 --- a/app/models/ckeditor/picture.rb +++ b/app/models/ckeditor/picture.rb @@ -1,12 +1,7 @@ class Ckeditor::Picture < ApplicationRecord # Ckeditor::Asset - # has_attached_file :data, - # :url => "/ckeditor_assets/pictures/:id/:style_:basename.:extension", - # :path => "/ckeditor_assets/pictures/:id/:style_:basename.:extension", - # :styles => { :content => '800>', :thumb => '118x100#' } - # - # validates_attachment_presence :data - # validates_attachment_size :data, :less_than => 2.megabytes - # validates_attachment_content_type :data, :content_type => /\Aimage/ + ACCEPTED_CONTENT_TYPES = ["image/jpeg", "image/png"].freeze + has_one_attached :data + validates :data, size: {less_than: 2.megabytes}, content_type: ACCEPTED_CONTENT_TYPES, attached: true def url_content url(:content) @@ -20,6 +15,5 @@ def url(param) else actual_url.gsub("/original", "/thumb") end - end end diff --git a/app/models/image.rb b/app/models/image.rb index 50d64f2ec..67148f2ef 100644 --- a/app/models/image.rb +++ b/app/models/image.rb @@ -2,11 +2,7 @@ class Image < ApplicationRecord belongs_to :owner, polymorphic: true belongs_to :report, optional: true - has_attached_file :file, - styles: { medium: '300x300>', thumb: '100x100>' }, - default_url: - ActionController::Base.helpers.asset_path( - 'workshop_default.png' - ) - validates_attachment :file, content_type: { content_type: ["image/jpg", "image/jpeg", "image/png", "image/gif"] } + ACCEPTED_CONTENT_TYPES = ["image/jpeg", "image/png", "image/gif"].freeze + has_one_attached :file + validates :file, content_type: ACCEPTED_CONTENT_TYPES end diff --git a/app/models/media_file.rb b/app/models/media_file.rb index 6a40148e5..e94122b8e 100644 --- a/app/models/media_file.rb +++ b/app/models/media_file.rb @@ -3,13 +3,11 @@ class MediaFile < ApplicationRecord belongs_to :workshop, optional: true belongs_to :workshop_log, optional: true - has_attached_file :file - - FORM_FILE_CONTENT_TYPES = ["image/jpeg", "image/jpg", "image/png", - "application/pdf", "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] - - validates_attachment :file, content_type: { content_type: FORM_FILE_CONTENT_TYPES } + FORM_FILE_CONTENT_TYPES = ["image/jpeg", "image/png", + "application/pdf", "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] + has_one_attached :file + validates :file, content_type: FORM_FILE_CONTENT_TYPES end diff --git a/app/models/project_user.rb b/app/models/project_user.rb index bb1a2fc57..c22e466bb 100644 --- a/app/models/project_user.rb +++ b/app/models/project_user.rb @@ -3,7 +3,7 @@ class ProjectUser < ApplicationRecord belongs_to :project belongs_to :user - scope :liaisons, -> { where(position: 1) } + scope :liaisons, -> { where(position: "liaison") } # Validations validates_presence_of :project_id diff --git a/app/models/report.rb b/app/models/report.rb index 767456783..f1e54ce1d 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -4,16 +4,17 @@ class Report < ApplicationRecord belongs_to :windows_type belongs_to :owner, polymorphic: true, optional: true - has_one :form, as: :owner - has_one :image + has_one :form, as: :owner + has_one :image validate :image_valid? - has_attached_file :form_file - before_save :set_has_attachament + FORM_FILE_CONTENT_TYPES = %w[application/pdf application/msword + application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet] + has_one_attached :form_file + validates :form_file, content_type: FORM_FILE_CONTENT_TYPES - FORM_FILE_CONTENT_TYPES = %w[application/pdf application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet] - - validates_attachment :form_file, content_type: { content_type: FORM_FILE_CONTENT_TYPES } + before_save :set_has_attachament # TODO verify set_has_attachament works as expected once this feature is enabled in the UI has_many :images has_many :form_fields, through: :form @@ -23,15 +24,15 @@ class Report < ApplicationRecord has_many :notifications, as: :noticeable, dependent: :destroy has_many :sectorable_items, as: :sectorable, dependent: :destroy has_many :sectors, through: :sectorable_items, dependent: :destroy - scope :in_month, -> (date) { where(created_at: date.beginning_of_month..date.end_of_month) } + scope :in_month, ->(date) { where(created_at: date.beginning_of_month..date.end_of_month) } has_many :media_files, dependent: :destroy accepts_nested_attributes_for :media_files accepts_nested_attributes_for :report_form_field_answers, - reject_if: proc { |object| - (object['_create'].to_i == 0 && object['answer'].nil?) - } + reject_if: proc { |object| + object["_create"].to_i == 0 && object["answer"].nil? + } accepts_nested_attributes_for :quotable_item_quotes after_create :set_windows_type @@ -45,13 +46,13 @@ def users_admin_type when "MonthlyReport" "#{type} - Monthly Report Date: #{date_label} - User: #{user.full_name if user}" when "Report" - if owner_type and owner_type == 'Resource' - "#{type} - #{owner ? owner_type : '[ EMPTY ]'} - User: #{user.full_name if user}" + if owner_type and owner_type == "Resource" + "#{type} - #{owner ? owner_type : "[ EMPTY ]"} - User: #{user.full_name if user}" else - "#{type} - #{owner ? owner.type : '[ EMPTY ]'} - User: #{user.full_name if user}" + "#{type} - #{owner ? owner.type : "[ EMPTY ]"} - User: #{user.full_name if user}" end else - "#{type} - #{owner ? owner.communal_label(self) : '[ EMPTY ]'} - User: #{user.full_name if user}" + "#{type} - #{owner ? owner.communal_label(self) : "[ EMPTY ]"} - User: #{user.full_name if user}" end end end @@ -61,7 +62,7 @@ def story? end def date_label - "#{date ? date.strftime('%m/%d/%y') : '[ EMPTY ]'}" + "#{date ? date.strftime("%m/%d/%y") : "[ EMPTY ]"}" end def delete_and_update_all(quotes_params, log_fields, image = nil) @@ -73,7 +74,7 @@ def delete_and_update_all(quotes_params, log_fields, image = nil) unless image.blank? self.image.destroy if self.image - self.image = Image.new( file: image ) + self.image = Image.new(file: image) end save @@ -90,8 +91,8 @@ def image_valid? def log_fields if form_builder - form_builder.forms[0].form_fields.where('ordering is not null and status = 1'). - order(ordering: :desc).all + form_builder.forms[0].form_fields.where("ordering is not null and status = 1") + .order(ordering: :desc).all else [] end @@ -105,7 +106,7 @@ def form_builder FormBuilder.find(2) end elsif owner and owner_type.include? "FormBuilder" - FormBuilder.find(7) + FormBuilder.find(7) else nil end @@ -120,21 +121,18 @@ def title end def name - type ? type.titleize : 'Report' + type ? type.titleize : "Report" end def display_date - created_at.strftime('%B %e, %Y') + created_at.strftime("%B %e, %Y") end private def set_has_attachament - self.has_attachment = false - - unless self.image.blank? and self.media_files.empty? and self.form_file.blank? - self.has_attachment = true - end + self.has_attachment = image.attached? || form_file.attached? || + media_files.any? { |media_file| media_file.file.attached? } end def set_windows_type diff --git a/app/models/resource.rb b/app/models/resource.rb index 3851770b9..daf171135 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -1,4 +1,6 @@ class Resource < ApplicationRecord + include Rails.application.routes.url_helpers + # Associations belongs_to :user belongs_to :workshop, optional: true @@ -81,12 +83,10 @@ def main_image end def main_image_url - return main_image.file.url if main_image - - if self.kind == "Theme" - ActionController::Base.helpers.asset_path("workshop_default.png") + if main_image&.file&.attached? + url_for(main_image.file) else - "/images/workshop_default.jpg" + ActionController::Base.helpers.asset_path("workshop_default.png") end end diff --git a/app/models/user.rb b/app/models/user.rb index 5013dc4e8..56f811843 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,6 +7,11 @@ class User < ApplicationRecord after_create :set_default_values before_destroy :reassign_reports_and_logs_to_orphaned_user + # Avatar + ACCEPTED_CONTENT_TYPES = ["image/jpeg", "image/png" ].freeze + has_one_attached :avatar + validates :avatar, content_type: ACCEPTED_CONTENT_TYPES + # Associations belongs_to :facilitator, optional: true has_many :workshops @@ -14,9 +19,9 @@ class User < ApplicationRecord has_many :reports has_many :communal_reports, through: :projects, source: :reports has_many :bookmarks, dependent: :destroy - has_many :bookmarked_workshops, through: :bookmarks, source: :bookmarkable, source_type: 'Workshop' - has_many :bookmarked_resources, through: :bookmarks, source: :bookmarkable, source_type: 'Resource' - has_many :bookmarked_events, through: :bookmarks, source: :bookmarkable, source_type: 'Event' + has_many :bookmarked_workshops, through: :bookmarks, source: :bookmarkable, source_type: "Workshop" + has_many :bookmarked_resources, through: :bookmarks, source: :bookmarkable, source_type: "Resource" + has_many :bookmarked_events, through: :bookmarks, source: :bookmarkable, source_type: "Event" has_many :project_users, dependent: :destroy has_many :projects, through: :project_users has_many :windows_types, through: :projects @@ -28,10 +33,6 @@ class User < ApplicationRecord has_many :colleagues, -> { select(:user_id, :position, :project_id).distinct }, through: :projects, source: :project_users has_many :notifications, as: :noticeable - # Avatar - has_attached_file :avatar, styles: { medium: "300x300>", thumb: "100x100>" }, default_url: "/images/missing.png" - validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\z/ - # Nested accepts_nested_attributes_for :user_forms accepts_nested_attributes_for :project_users, reject_if: :all_blank, allow_destroy: true @@ -142,7 +143,7 @@ def name end def agency_name - agency ? agency.name : 'No agency.' + agency ? agency.name : "No agency." end def has_bookmarkable?(bookmarkable, type: nil) @@ -168,6 +169,7 @@ def set_default_values adult_perm = Permission.find_by(security_cat: "Adult Windows") children_perm = Permission.find_by(security_cat: "Children's Windows") + self.permissions << combined_perm self.permissions << adult_perm self.permissions << children_perm diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 3446b131d..0ee7270fe 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -1,4 +1,6 @@ class Workshop < ApplicationRecord + include Rails.application.routes.url_helpers + attr_accessor :time_hours, :time_minutes before_save :set_time_frame @@ -7,6 +9,13 @@ class Workshop < ApplicationRecord belongs_to :user, optional: true belongs_to :windows_type + ACCEPTED_CONTENT_TYPES = ["image/jpeg", "image/png" ].freeze + has_one_attached :thumbnail + validates :thumbnail, content_type: ACCEPTED_CONTENT_TYPES + + has_one_attached :header + validates :header, content_type: ACCEPTED_CONTENT_TYPES + has_many :sectorable_items, dependent: :destroy, inverse_of: :sectorable, as: :sectorable @@ -43,32 +52,25 @@ class Workshop < ApplicationRecord foreign_key: "workshop_child_id", dependent: :destroy - has_attached_file :thumbnail, default_url: "/images/workshop_default.jpg" - validates_attachment_content_type :thumbnail, content_type: /\Aimage\/.*\Z/ - - has_attached_file :header, default_url: "/images/workshop_default.jpg" - validates_attachment_content_type :header, content_type: /\Aimage\/.*\Z/ - - # Nested Attributes accepts_nested_attributes_for :images, reject_if: :all_blank, allow_destroy: true accepts_nested_attributes_for :sectorable_items, - reject_if: proc { |object| object['_create'] == '0' }, - allow_destroy: true + reject_if: proc { |object| object["_create"] == "0" }, + allow_destroy: true accepts_nested_attributes_for :sectors, - reject_if: proc { |object| object['_create'] == '0' || !object['_create'] }, - allow_destroy: true + reject_if: proc { |object| object["_create"] == "0" || !object["_create"] }, + allow_destroy: true accepts_nested_attributes_for :workshop_age_ranges, - reject_if: proc { |object| - object['_create'] == '0' || !object['_create'] || - WorkshopAgeRange.find_by(workshop_id: object[:workshop_id], age_range_id: object[:age_range_id]); - }, - allow_destroy: true + reject_if: proc { |object| + object["_create"] == "0" || !object["_create"] || + WorkshopAgeRange.find_by(workshop_id: object[:workshop_id], age_range_id: object[:age_range_id]) + }, + allow_destroy: true accepts_nested_attributes_for :quotes, - reject_if: proc { |object| object['quote'].nil? } + reject_if: proc { |object| object["quote"].nil? } accepts_nested_attributes_for :workshop_variations, - reject_if: proc { |object| object.nil? } + reject_if: proc { |object| object.nil? } accepts_nested_attributes_for :workshop_series_children, reject_if: proc { |attributes| attributes['workshop_child_id'].blank? }, @@ -83,14 +85,14 @@ class Workshop < ApplicationRecord # Validations validates_presence_of :title - #validates_presence_of :month, :year, if: Proc.new { |workshop| workshop.legacy } + # validates_presence_of :month, :year, if: Proc.new { |workshop| workshop.legacy } validates_length_of :age_range, maximum: 16 validates :rating, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 5 } # Nested Attributes accepts_nested_attributes_for :workshop_logs, - reject_if: :all_blank, - allow_destroy: true + reject_if: :all_blank, + allow_destroy: true attr_writer :time_hours, :time_minutes @@ -135,8 +137,8 @@ def name def workshop_log_fields if form_builder - form_builder.forms[0].form_fields.where('ordering is not null and status = 1'). - order(ordering: :desc).all + form_builder.forms[0].form_fields.where("ordering is not null and status = 1") + .order(ordering: :desc).all else [] end @@ -158,7 +160,7 @@ def related_workshops def default_image_url ActionController::Base.helpers.asset_path( - 'workshop_default.jpg' + "workshop_default.jpg" ) end @@ -172,17 +174,17 @@ def log_count end def main_image_url - if legacy - decorate.main_image - elsif images.first - "http://awbw-production.herokuapp.com#{images.first.file.url}" + if images.attached? && images.first.present? + url_for(images.first) + else + ActionController::Base.helpers.asset_path("workshop_default.png") end end def sector_hashtags sectors.map do |sector| - "\##{sector.name.split(' ')[0].downcase}" - end.join(',') + "\#{sector.name.split(" ")[0].downcase}" + end.join(",") end def admin_title @@ -194,7 +196,7 @@ def log_title end def communal_label(report) - "Workshop Title: #{title} - Workshop Date: #{report.date ? report.date.strftime('%m/%d/%y') : '[ EMPTY ]'}" + "Workshop Title: #{title} - Workshop Date: #{report.date ? report.date.strftime("%m/%d/%y") : "[ EMPTY ]"}" end def published_sectors @@ -202,13 +204,13 @@ def published_sectors end def time_frame_total - total_time = time_intro.to_i + time_demonstration.to_i + - time_opening.to_i + time_warm_up.to_i + - time_creation.to_i + time_closing.to_i + total_time = time_intro.to_i + time_demonstration.to_i + + time_opening.to_i + time_warm_up.to_i + + time_creation.to_i + time_closing.to_i - return ("00:00") if total_time == 0 + return "00:00" if total_time == 0 - Time.at(total_time * 60).utc #.strftime("%H:%M") + Time.at(total_time * 60).utc # .strftime("%H:%M") end def set_time_frame @@ -219,18 +221,18 @@ def set_time_frame def self.anchors_english_admin "".html_safe - %w(extra_field objective materials optional_materials setup + %w[extra_field objective materials optional_materials setup introduction demonstration opening_circle warm_up visualization creation - closing notes tips).map {|field| - "#{field.capitalize} |"}.join(" ").html_safe + closing notes tips].map { |field| + "#{field.capitalize} |" }.join(" ").html_safe end - def self.anchors_spanish_admin - %w(extra_field_spanish objective_spanish materials_spanish optional_materials_spanish + def self.anchors_spanish_admin + %w[extra_field_spanish objective_spanish materials_spanish optional_materials_spanish timeframe_spanish age_range_spanish setup_spanish introduction_spanish demonstration_spanish opening_circle_spanish warm_up_spanish visualization_spanish creation_spanish closing_spanish notes_spanish tips_spanish misc1_spanish - misc2_spanish).map {|field| - "#{field.capitalize} |"}.join(" ").html_safe - end + misc2_spanish].map { |field| + "#{field.capitalize} |" }.join(" ").html_safe + end end diff --git a/app/views/dashboard/_featured_workshops.html.erb b/app/views/dashboard/_featured_workshops.html.erb index b969c63aa..9c6d87472 100644 --- a/app/views/dashboard/_featured_workshops.html.erb +++ b/app/views/dashboard/_featured_workshops.html.erb @@ -117,11 +117,11 @@ <% @featured_workshops.each do |workshop| %>
  • <%= link_to workshop_path(workshop), { class: "nodecor"} do %> + <% thumbnail_image = url_for(workshop.thumbnail_image) %>
    -
    + class="portrait-img" + style="background-image: url(<%= thumbnail_image %>);" + >
    <%= workshop.windows_type.label if workshop.windows_type %> <%= workshop.date %>
    diff --git a/app/views/shared/_navbar_user.html.erb b/app/views/shared/_navbar_user.html.erb index 5113beef0..1b3619420 100644 --- a/app/views/shared/_navbar_user.html.erb +++ b/app/views/shared/_navbar_user.html.erb @@ -10,10 +10,11 @@ > Open user menu - <%= image_tag current_user.avatar.url(:thumb), + <% image = current_user.avatar.attached? ? url_for(current_user.avatar) : "missing.png" %> + <%= image_tag image, id: "avatar-image", - class: - "size-10 rounded-full bg-gray-800 outline -outline-offset-1 outline-white/10" %> + class: "size-10 rounded-full bg-gray-800 outline -outline-offset-1 outline-white/10 img-circle avatar" %> +