diff --git a/Dockerfile b/Dockerfile index b01252e89..fed3244cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,6 @@ FROM base AS assets RUN apt-get update -qq && apt-get install -y \ build-essential \ nodejs \ - imagemagick \ libvips \ poppler-utils \ tzdata \ @@ -53,7 +52,6 @@ RUN SECRET_KEY_BASE=1 \ FROM base AS server RUN apt-get update -qq && apt-get install --no-install-recommends -y \ - imagemagick \ libvips \ poppler-utils \ tzdata \ diff --git a/Dockerfile.dev b/Dockerfile.dev index 4c7963f03..dd2c56a6e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -6,7 +6,6 @@ RUN apt-get update -qq && apt-get install -y \ git \ curl \ default-libmysqlclient-dev \ - imagemagick \ libvips \ tzdata \ libxml2-dev \ diff --git a/app/controllers/community_news_controller.rb b/app/controllers/community_news_controller.rb index a07116777..5a087c6fd 100644 --- a/app/controllers/community_news_controller.rb +++ b/app/controllers/community_news_controller.rb @@ -93,7 +93,7 @@ def set_community_news # Strong parameters def community_news_params params.require(:community_news).permit( - :title, :rhino_body, :published, :featured, + :title, :rhino_body, :published, :featured, :public, :public_featured, :reference_url, :youtube_url, :project_id, :author_id, :created_by_id, :updated_by_id diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index d4c1c9d68..16eccb191 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -7,8 +7,8 @@ def index if turbo_frame_request? case turbo_frame_request_id when "dashboard_workshops" - ids = Rails.cache.fetch("featured_and_visitor_featured_workshop_ids", expires_in: 1.year) do - Workshop.featured_or_visitor_featured.pluck(:id) + ids = Rails.cache.fetch("featured_and_public_featured_workshop_ids", expires_in: 1.year) do + Workshop.featured_or_public_featured.pluck(:id) end base_scope = Workshop.includes(:bookmarks, :windows_type, :primary_asset) diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index 80f542c43..e3c5a5dfe 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -74,7 +74,7 @@ def destroy # Optional hooks for setting variables for forms or index def set_form_variables - @events = Event.publicly_visible.order(:start_date) + @events = Event.where(inactive: false).order(:start_date) @registrants = User.active.order(:last_name, :first_name) end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index fd4a35933..2a2f4df7e 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,25 +1,28 @@ class EventsController < ApplicationController include AhoyViewTracking, AssetUpdatable + skip_before_action :authenticate_user!, only: %i[ index show] before_action :set_event, only: %i[ show edit update destroy ] - before_action :authorize_admin!, only: %i[ edit update destroy ] def index - unpaginated = current_user.super_user? ? Event.all : Event.published - unpaginated = unpaginated.search_by_params(params) - @events = unpaginated.order(start_date: :desc) + authorize! + base_scope = authorized_scope(Event.all) + @events = base_scope.search_by_params(params).order(start_date: :desc) end def show + authorize! @event @event = @event.decorate track_view(@event) end - def new # all logged in users can create events + def new + authorize! @event = Event.new.decorate set_form_variables end def edit + authorize! @event set_form_variables unless @event.created_by == current_user || current_user.super_user? redirect_to events_path, alert: "You are not authorized to edit this event." @@ -27,6 +30,7 @@ def edit end def create + authorize! @event = Event.new(event_params).decorate @event.created_by ||= current_user @@ -46,6 +50,7 @@ def create end def update + authorize! @event respond_to do |format| if @event.update(event_params) format.html { redirect_to events_path, notice: "Event was successfully updated." } @@ -59,6 +64,7 @@ def update end def destroy + authorize! @event @event.destroy respond_to do |format| @@ -87,12 +93,9 @@ def event_params :featured, :start_date, :end_date, :registration_close_date, - :publicly_visible + :inactive, + :public, + :public_featured ) end - - def authorize_admin! - redirect_to events_path, - alert: "You are not authorized to perform this action." unless current_user.super_user? - end end diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb index 071cbb1d6..f82cccda4 100644 --- a/app/controllers/resources_controller.rb +++ b/app/controllers/resources_controller.rb @@ -147,7 +147,7 @@ def resource_id_param def resource_params params.require(:resource).permit( - :rhino_text, :kind, :male, :female, :title, :featured, :inactive, :url, + :rhino_text, :kind, :male, :female, :title, :featured, :inactive, :public, :public_featured, :url, :agency, :author, :filemaker_code, :windows_type_id, :position, categorizable_items_attributes: [ :id, :category_id, :_destroy ], category_ids: [], sectorable_items_attributes: [ :id, :sector_id, :is_leader, :_destroy ], sector_ids: [] diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 0f37da84f..c442bdc89 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -110,7 +110,7 @@ def set_story # Strong parameters def story_params params.require(:story).permit( - :title, :rhino_body, :featured, :published, :youtube_url, :website_url, + :title, :rhino_body, :featured, :published, :public, :public_featued, :youtube_url, :website_url, :windows_type_id, :project_id, :workshop_id, :external_workshop_title, :created_by_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id ) diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb index 7a3476b2b..429093615 100644 --- a/app/controllers/workshops_controller.rb +++ b/app/controllers/workshops_controller.rb @@ -239,6 +239,8 @@ def workshop_params :title, :featured, :inactive, :full_name, :user_id, :windows_type_id, :workshop_idea_id, :month, :year, + :public, + :public_featured, :time_intro, :time_closing, :time_creation, :time_demonstration, :time_warm_up, :time_opening, :time_opening_circle, diff --git a/app/models/community_news.rb b/app/models/community_news.rb index ec70cf65d..a9772e8a4 100644 --- a/app/models/community_news.rb +++ b/app/models/community_news.rb @@ -43,7 +43,7 @@ class CommunityNews < ApplicationRecord end scope :featured, -> { where(featured: true) } - scope :visitor_featured, -> { where(visitor_featured: true) } + scope :public_featured, -> { where(public_featured: true) } scope :category_names, ->(names) { tag_names(:categories, names) } scope :sector_names, ->(names) { tag_names(:sectors, names) } scope :community_news_name, ->(community_news_name) { diff --git a/app/models/event.rb b/app/models/event.rb index 84681c227..796245af1 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -20,7 +20,7 @@ class Event < ApplicationRecord # Validations validates_presence_of :title, :start_date, :end_date - validates_inclusion_of :publicly_visible, in: [ true, false ] + validates_inclusion_of :inactive, in: [ true, false ] validates_numericality_of :cost_cents, greater_than_or_equal_to: 0, allow_nil: true # Nested attributes @@ -33,13 +33,24 @@ class Event < ApplicationRecord attributes :title, :description end - scope :featured, -> { where(featured: true) } - scope :visitor_featured, -> { where(visitor_featured: true) } - scope :published, ->(published = nil) { publicly_visible(published) } - scope :publicly_visible, ->(publicly_visible = nil) { publicly_visible ? where(publicly_visible: publicly_visible): where(publicly_visible: true) } + + # Action Policy + scope :featured, -> { + where(featured: true) + .where("registration_close_date IS NULL OR registration_close_date >= ?", Time.current) + } + scope :public_featured, -> { + where(public: true, public_featured: true) + .where("registration_close_date IS NULL OR registration_close_date >= ?", Time.current) + } + + + scope :published, ->(published = nil) { published.to_s.present? ? + where(inactive: !published) : where(inactive: false) } scope :category_names, ->(names) { tag_names(:categories, names) } scope :sector_names, ->(names) { tag_names(:sectors, names) } + def self.search_by_params(params) stories = self.all stories = stories.search(params[:query]) if params[:query].present? @@ -49,12 +60,8 @@ def self.search_by_params(params) stories end - def inactive? - !publicly_visible - end - def registerable? - publicly_visible && + !inactive && (registration_close_date.nil? || registration_close_date >= Time.current) end diff --git a/app/models/resource.rb b/app/models/resource.rb index 7cb0b6a30..65cc819b0 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -77,7 +77,7 @@ class Resource < ApplicationRecord scope :category_names, ->(names) { tag_names(:categories, names) } scope :sector_names, ->(names) { tag_names(:sectors, names) } scope :featured, ->(featured = nil) { featured.present? ? where(featured: featured) : where(featured: true) } - scope :visitor_featured, -> { where(visitor_featured: true) } + scope :public_featured, -> { where(public_featured: true) } scope :kinds, ->(kinds) { kinds = Array(kinds).flatten.map(&:to_s) where(kind: kinds) diff --git a/app/models/story.rb b/app/models/story.rb index b7fde0746..6320c1a9b 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -50,7 +50,7 @@ class Story < ApplicationRecord # Scopes scope :featured, -> { where(featured: true) } - scope :visitor_featured, -> { where(visitor_featured: true) } + scope :public_featured, -> { where(public_featured: true) } scope :category_names, ->(names) { tag_names(:categories, names) } scope :sector_names, ->(names) { tag_names(:sectors, names) } scope :story_name, ->(story_name) { diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 0bcc7534b..f1b0416e5 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -119,7 +119,7 @@ class Workshop < ApplicationRecord scope :sector_names, ->(names) { tag_names(:sectors, names) } scope :created_by_id, ->(created_by_id) { where(user_id: created_by_id) } scope :featured, -> { where(featured: true) } - scope :visitor_featured, -> { where(visitor_featured: true) } + scope :public_featured, -> { where(public_featured: true) } scope :legacy, -> { where(legacy: true) } scope :published, ->(published = nil) { published.to_s.present? ? where(inactive: !published) : where(inactive: false) } @@ -141,8 +141,8 @@ class Workshop < ApplicationRecord .select("workshops.*, COUNT(bookmarks.id) AS bookmarks_count") .group("workshops.id") } - scope :featured_or_visitor_featured, -> { - where("(featured = ? OR visitor_featured = ?) AND inactive = ?", true, true, false) + scope :featured_or_public_featured, -> { + where("(featured = ? OR public_featured = ?) AND inactive = ?", true, true, false) } # Search Cop @@ -282,13 +282,13 @@ def attachable_content_type end def invalidate_featured_cache_if_changed - if featured_or_visitor_featured_changed? - Rails.cache.delete("featured_and_visitor_featured_workshop_ids") + if featured_or_public_featured_changed? + Rails.cache.delete("featured_and_public_featured_workshop_ids") end end - def featured_or_visitor_featured_changed? - featured_changed? || visitor_featured_changed? || inactive_changed? + def featured_or_public_featured_changed? + featured_changed? || public_featured_changed? || inactive_changed? end def attach_assets_from_idea! diff --git a/app/policies/dashboard_policy.rb b/app/policies/dashboard_policy.rb index 009322416..da78cb6cc 100644 --- a/app/policies/dashboard_policy.rb +++ b/app/policies/dashboard_policy.rb @@ -9,7 +9,7 @@ def index? if authenticated? relation.featured else - relation.visitor_featured + relation.public_featured end end end diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb new file mode 100644 index 000000000..e47272f8d --- /dev/null +++ b/app/policies/event_policy.rb @@ -0,0 +1,27 @@ +class EventPolicy < ApplicationPolicy + # See https://actionpolicy.evilmartians.io/#/writing_policies + # + # override or add new rules here that are not defined in ApplicationPolicy + + skip_pre_check :verify_authenticated! + + def show? + admin? || + record.public? || + (!record.inactive? && authenticated?) + end + + relation_scope do |relation| + if admin? + relation + elsif authenticated? + relation.left_outer_joins(:registrants) + .where(inactive: false) + .where("registration_close_date IS NULL OR registration_close_date >= ? OR users.id = ?", Time.current, user.id) + .distinct + else + relation.where(inactive: false) + .where("registration_close_date IS NULL OR registration_close_date >= ?", Time.current) + end + end +end diff --git a/app/views/bookmarks/_editable_bookmark_button.html.erb b/app/views/bookmarks/_editable_bookmark_button.html.erb index 5bdb604d6..18408f7e8 100644 --- a/app/views/bookmarks/_editable_bookmark_button.html.erb +++ b/app/views/bookmarks/_editable_bookmark_button.html.erb @@ -1,8 +1,8 @@ -<% resource = Draper.undecorate(resource) %> - -<%= tag.span id: dom_id(resource, :bookmark_button), data: {controller: "optimistic-bookmark"} do %> - <% if resource.bookmarks.any? { |b| b.user_id == current_user.id } %> - <%= button_to bookmark_path(current_user.bookmark_for(resource)), +<% if allowed_to?(:update?, Bookmark) %> + <% resource = Draper.undecorate(resource) %> + <%= tag.span id: dom_id(resource, :bookmark_button), data: {controller: "optimistic-bookmark"} do %> + <% if resource.bookmarks.any? { |b| b.user_id == current_user.id } %> + <%= button_to bookmark_path(current_user.bookmark_for(resource)), method: :delete, class: "inline-flex items-center gap-2 px-4 py-2 border border-gray-500 text-primary rounded-lg @@ -12,18 +12,19 @@ turbo_confirm: "Remove bookmark?", action: "click->optimistic-bookmark#toggle" } do %> - - Bookmarked - <% end %> - <% else %> - <%= button_to bookmarks_path(bookmark: { bookmarkable_id: resource.id, bookmarkable_type: resource.class.name }), + + Bookmarked + <% end %> + <% else %> + <%= button_to bookmarks_path(bookmark: { bookmarkable_id: resource.id, bookmarkable_type: resource.class.name }), class: "inline-flex items-center gap-2 px-4 py-2 border border-gray-500 text-primary rounded-lg hover:bg-primary hover:text-white transition-colors duration-200 font-medium shadow-sm leading-none", data: { action: "click->optimistic-bookmark#toggle" } do %> - - Bookmark + + Bookmark + <% end %> <% end %> <% end %> <% end %> diff --git a/app/views/community_news/_form.html.erb b/app/views/community_news/_form.html.erb index d9423f48d..ed1ba1048 100644 --- a/app/views/community_news/_form.html.erb +++ b/app/views/community_news/_form.html.erb @@ -10,10 +10,17 @@
<%= f.input :title, as: :text, input_html: { rows: 1, class: "w-full" } %> -
- <%= f.input :published, as: :boolean, wrapper_html: { class: "flex items-center" } %> - <%= f.input :featured, as: :boolean, wrapper_html: { class: "flex items-center" } %> + <%# if allowed_to?(:manage?, @commenity_news) %> +
+ <%= f.input :published, as: :boolean %> + + <%# f.input :inactive, as: :boolean, label: "Hidden?" %> + <%= f.input :featured, as: :boolean, label: "Featured?" %> + <%= f.input :public, as: :boolean, label: "Public?" %> + <%= f.input :public_featured, as: :boolean, label: "Public Featured?" %>
+ + <%# end %>
<%= rhino_editor(f, :body, label: "Body / Description") %> diff --git a/app/views/events/_card.html.erb b/app/views/events/_card.html.erb index afb03b0d3..246f955e1 100644 --- a/app/views/events/_card.html.erb +++ b/app/views/events/_card.html.erb @@ -41,7 +41,7 @@
- <% if current_user.super_user? %> + <% if allowed_to?(:edit?, event) %> <%= link_to "Edit", edit_event_path(event), data: { turbo_frame: "_top"}, diff --git a/app/views/events/_form.html.erb b/app/views/events/_form.html.erb index 5ce00c953..4bfb5db4a 100644 --- a/app/views/events/_form.html.erb +++ b/app/views/events/_form.html.erb @@ -20,10 +20,12 @@
- <% if current_user.super_user? %> -
- <%= f.input :publicly_visible, as: :boolean, label: "Publicly visible?", wrapper_html: { class: "ml-2" } %> - <%= f.input :featured, as: :boolean, label: "Featured?", wrapper_html: { class: "ml-2" } %> + <% if allowed_to?(:manage?, @event) %> +
+ <%= f.input :inactive, as: :boolean, label: "Hidden?" %> + <%= f.input :featured, as: :boolean, label: "Featured?" %> + <%= f.input :public, as: :boolean, label: "Public?" %> + <%= f.input :public_featured, as: :boolean, label: "Public Featured?" %>
<% end %>
diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 80b925da1..2bfdfa502 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -17,7 +17,7 @@
- <% if current_user.super_user? %> + <% if allowed_to?(:new?, Event) %> <%= link_to "New Event", new_event_path, class: "whitespace-nowrap btn btn-secondary-outline" %> diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 37c658144..926c7e233 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -1,53 +1,39 @@
-
-
<%= link_to "Index", events_path, class: "btn btn-secondary-outline" %> - <% if current_user.super_user? %> + <% if allowed_to?(:edit, @event) %> <%= link_to "Edit", edit_event_path(@event), class: "btn btn-secondary-outline admin-only bg-blue-100" %> <% end %> - - <%= render "bookmarks/editable_bookmark_button", resource: @event.object %> - + <%= render "bookmarks/editable_bookmark_button", resource: @event.object %>
-
-
- -
- <%= title_with_badges(@event, font_size: "text-3xl") %> -
+
<%= title_with_badges(@event, font_size: "text-3xl") %>
- -
- <%= @event.times(display_day: true, display_date: true) %> -
+
<%= @event.times(display_day: true, display_date: true) %>
<% if @event.labelled_cost %> -
- <%= @event.labelled_cost %> -
+
<%= @event.labelled_cost %>
<% end %>
- <% registered = @event.event_registrations.exists?(registrant_id: current_user.id) %> + <% registered = @event.event_registrations.exists?(registrant_id: current_user&.id) %> <% if registered %> <%= button_to "De-register", @@ -55,7 +41,6 @@ method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "btn btn-secondary-outline" %> - <% elsif @event.registerable? %> <%= button_to "Register", event_registrant_registration_path(event_id: @event), @@ -73,22 +58,19 @@ <% if registered %> -
- <%= @event.calendar_links %> -
+
<%= @event.calendar_links %>
<% end %> -
<% if @event.registration_close_date %>

Registration Close Date

+

<%= @event.registration_close_date&.strftime("%B %-d, %Y %l:%M %P") || "—" %>

<% end %> -
@@ -101,11 +83,8 @@
-

- <%= @event.detail.presence || "—" %> -

+

<%= @event.detail.presence || "—" %>

-
diff --git a/app/views/resources/_form.html.erb b/app/views/resources/_form.html.erb index 484819ff9..f37bf7781 100644 --- a/app/views/resources/_form.html.erb +++ b/app/views/resources/_form.html.erb @@ -16,15 +16,14 @@
-
-
<%= f.input :featured, as: :boolean, wrapper: false, label_html: { class: "inline" } %>
- -
- <%= f.input :inactive, as: :boolean, - label: "Hidden?", - wrapper: false, label_html: { class: "inline" } %> + <% if allowed_to?(:manage?, @resource) %> +
+ <%= f.input :inactive, as: :boolean, label: "Hidden?" %> + <%= f.input :featured, as: :boolean, label: "Featured?" %> + <%= f.input :public, as: :boolean, label: "Public?" %> + <%= f.input :public_featured, as: :boolean, label: "Public Featured?" %>
-
+ <% end %>
<% if f.object.url.present? %> diff --git a/app/views/stories/_form.html.erb b/app/views/stories/_form.html.erb index fb42e9371..f8c564281 100644 --- a/app/views/stories/_form.html.erb +++ b/app/views/stories/_form.html.erb @@ -11,10 +11,17 @@ input_html: { rows: 2, value: f.object.title || @story_idea&.title } %>
-
- <%= f.input :published, as: :boolean, wrapper_html: { class: "flex items-center" } %> - <%= f.input :featured, as: :boolean, wrapper_html: { class: "flex items-center" } %> + <%# if allowed_to?(:manage?, story) %> +
+ <%= f.input :published, as: :boolean %> + + <%# f.input :inactive, as: :boolean, label: "Hidden?" %> + <%= f.input :featured, as: :boolean, label: "Featured?" %> + <%= f.input :public, as: :boolean, label: "Public?" %> + <%= f.input :public_featured, as: :boolean, label: "Public Featured?" %>
+ + <%# end %>
@@ -126,23 +133,23 @@
- <% if story_idea %> -
- <%= label_tag :promote_idea_assets, class: "flex items-start space-x-3 cursor-pointer" do %> - <%= check_box_tag :promote_idea_assets, true, false, class: "mt-1 h-5 w-5 text-blue-600 rounded border-gray-300 focus:ring-blue-500" %> + <% if story_idea %> +
+ <%= label_tag :promote_idea_assets, class: "flex items-start space-x-3 cursor-pointer" do %> + <%= check_box_tag :promote_idea_assets, true, false, class: "mt-1 h-5 w-5 text-blue-600 rounded border-gray-300 focus:ring-blue-500" %> -
- - If the story idea was submitted with attachments, check this box to transfer them to this new story. - +
+ + If the story idea was submitted with attachments, check this box to transfer them to this new story. + -

- (This will override any attachments uploaded in the form below once you click submit.) -

-
- <% end %> -
- <% end %> +

+ (This will override any attachments uploaded in the form below once you click submit.) +

+
+ <% end %> +
+ <% end %> <% end %>
<%= render "assets/form", owner: @story %>
diff --git a/app/views/workshops/_form.html.erb b/app/views/workshops/_form.html.erb index 285c5055d..18772e13d 100644 --- a/app/views/workshops/_form.html.erb +++ b/app/views/workshops/_form.html.erb @@ -77,10 +77,15 @@
-
- <%= f.input :featured, as: :boolean, wrapper: false %> - <%= f.input :inactive, as: :boolean, label: "Hidden?", wrapper: false %> + <%# if allowed_to?(:manage?, @workshop) %> +
+ <%= f.input :inactive, as: :boolean, label: "Hidden?" %> + <%= f.input :featured, as: :boolean, label: "Featured?" %> + <%= f.input :public, as: :boolean, label: "Public?" %> + <%= f.input :public_featured, as: :boolean, label: "Public Featured?" %>
+ + <%# end %>
<% else %> <%= f.hidden_field :user_id, value: current_user&.id %> @@ -280,8 +285,11 @@ for="<%= id %>" class=" flex items-center gap-2 p-3 cursor-pointer w-auto min-w-[180px] bg-white border border-gray-200 - rounded-lg shadow-sm hover:bg-gray-100 transition"> - <%= hidden_field_tag "workshop[sector_ids][]", "" %> + rounded-lg shadow-sm hover:bg-gray-100 transition + " + > + <%= hidden_field_tag "workshop[sector_ids][]", "" %> + <%= check_box_tag "workshop[sector_ids][]", sector.id, @workshop.sector_ids.include?(sector.id), @@ -309,8 +317,11 @@ for="<%= id %>" class=" flex items-center gap-2 p-3 cursor-pointer w-auto min-w-[180px] bg-white border - border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 transition"> - <%= hidden_field_tag "workshop[category_ids][]", "" %> + border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 transition + " + > + <%= hidden_field_tag "workshop[category_ids][]", "" %> + <%= check_box_tag "workshop[category_ids][]", category.id, @workshop.category_ids.include?(category.id), diff --git a/db/migrate/20260202162104_rename_visitor_featured_to_public_featured.rb b/db/migrate/20260202162104_rename_visitor_featured_to_public_featured.rb new file mode 100644 index 000000000..ee998a61a --- /dev/null +++ b/db/migrate/20260202162104_rename_visitor_featured_to_public_featured.rb @@ -0,0 +1,9 @@ +class RenameVisitorFeaturedToPublicFeatured < ActiveRecord::Migration[8.1] + def change + rename_column :community_news, :visitor_featured, :public_featured + rename_column :events, :visitor_featured, :public_featured + rename_column :resources, :visitor_featured, :public_featured + rename_column :stories, :visitor_featured, :public_featured + rename_column :workshops, :visitor_featured, :public_featured + end +end diff --git a/db/migrate/20260202162452_add_public_to_dashboard_models.rb b/db/migrate/20260202162452_add_public_to_dashboard_models.rb new file mode 100644 index 000000000..2bd16e30d --- /dev/null +++ b/db/migrate/20260202162452_add_public_to_dashboard_models.rb @@ -0,0 +1,9 @@ +class AddPublicToDashboardModels < ActiveRecord::Migration[8.1] + def change + add_column :community_news, :public, :boolean, default: false, null: false + add_column :events, :public, :boolean, default: false, null: false + add_column :resources, :public, :boolean, default: false, null: false + add_column :stories, :public, :boolean, default: false, null: false + add_column :workshops, :public, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20260202164015_rename_publicly_visible_to_inactive_in_events.rb b/db/migrate/20260202164015_rename_publicly_visible_to_inactive_in_events.rb new file mode 100644 index 000000000..59bdcfb63 --- /dev/null +++ b/db/migrate/20260202164015_rename_publicly_visible_to_inactive_in_events.rb @@ -0,0 +1,6 @@ +class RenamePubliclyVisibleToInactiveInEvents < ActiveRecord::Migration[8.1] + def change + rename_column :events, :publicly_visible, :inactive + change_column_default :events, :inactive, true + end +end diff --git a/db/schema.rb b/db/schema.rb index 9153e0909..d61097724 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_01_27_171722) do +ActiveRecord::Schema[8.1].define(version: 2026_02_02_164015) do create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "action_text_rich_text_id", null: false t.datetime "created_at", null: false @@ -268,12 +268,13 @@ t.integer "created_by_id", null: false t.boolean "featured" t.integer "project_id" + t.boolean "public", default: false, null: false + t.boolean "public_featured", default: false, null: false t.boolean "published" t.string "reference_url" t.string "title" t.datetime "updated_at", null: false t.integer "updated_by_id", null: false - t.boolean "visitor_featured", default: false, null: false t.integer "windows_type_id" t.string "youtube_url" t.index ["author_id"], name: "index_community_news_on_author_id" @@ -316,12 +317,13 @@ t.text "description", size: :medium t.datetime "end_date", precision: nil t.boolean "featured", default: false, null: false - t.boolean "publicly_visible", default: false, null: false + t.boolean "inactive", default: true, null: false + t.boolean "public", default: false, null: false + t.boolean "public_featured", default: false, null: false t.datetime "registration_close_date", precision: nil t.datetime "start_date", precision: nil t.string "title" t.datetime "updated_at", null: false - t.boolean "visitor_featured", default: false, null: false t.index ["created_by_id"], name: "index_events_on_created_by_id" end @@ -663,12 +665,13 @@ t.integer "legacy_id" t.boolean "male", default: false t.integer "position" + t.boolean "public", default: false, null: false + t.boolean "public_featured", default: false, null: false t.text "text", size: :long t.string "title" t.datetime "updated_at", precision: nil, null: false t.string "url" t.integer "user_id" - t.boolean "visitor_featured", default: false, null: false t.integer "windows_type_id" t.integer "workshop_id" t.index ["user_id"], name: "index_resources_on_user_id" @@ -704,13 +707,14 @@ t.boolean "featured", default: false, null: false t.boolean "permission_given" t.integer "project_id" + t.boolean "public", default: false, null: false + t.boolean "public_featured", default: false, null: false t.boolean "published", default: false, null: false t.integer "spotlighted_facilitator_id" t.bigint "story_idea_id" t.string "title" t.datetime "updated_at", null: false t.integer "updated_by_id", null: false - t.boolean "visitor_featured", default: false, null: false t.string "website_url" t.integer "windows_type_id", null: false t.integer "workshop_id" @@ -1035,6 +1039,8 @@ t.text "project", size: :long t.text "project_spanish", size: :long t.string "pub_issue" + t.boolean "public", default: false, null: false + t.boolean "public_featured", default: false, null: false t.boolean "searchable", default: false t.text "setup", size: :long t.text "setup_spanish", size: :long @@ -1057,7 +1063,6 @@ t.string "title" t.datetime "updated_at", precision: nil, null: false t.integer "user_id" - t.boolean "visitor_featured", default: false, null: false t.text "visualization", size: :long t.text "visualization_spanish", size: :long t.text "warm_up", size: :long diff --git a/db/seeds/dummy_dev_seeds.rb b/db/seeds/dummy_dev_seeds.rb index 573a021e8..9d5b4ef73 100644 --- a/db/seeds/dummy_dev_seeds.rb +++ b/db/seeds/dummy_dev_seeds.rb @@ -106,7 +106,7 @@ .first_or_create!( description: Faker::Lorem.paragraph(sentence_count: 6), featured: [ true, false ].sample, - publicly_visible: [ true, true, false ].sample, + inactive: [ false, false, true ].sample, registration_close_date: registration_close, created_by_id: User.first.id, created_at: Time.current - rand(10..90).days, diff --git a/spec/factories/events.rb b/spec/factories/events.rb index 7773d5cc1..2145365b4 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -5,7 +5,7 @@ start_date { 12.day.from_now } end_date { 14.days.from_now } registration_close_date { 13.days.from_now } - publicly_visible { true } + inactive { false } cost_cents { 1099 } trait :registration_closed do diff --git a/spec/policies/dashboard_policy_spec.rb b/spec/policies/dashboard_policy_spec.rb index d6b522071..da853e832 100644 --- a/spec/policies/dashboard_policy_spec.rb +++ b/spec/policies/dashboard_policy_spec.rb @@ -50,9 +50,9 @@ def policy_for(record: nil, user:) context "without user" do let(:policy) { policy_for(record: Workshop, user: nil) } - it "returns visitor_featured scope for unauthenticated users" do + it "returns public_featured scope for unauthenticated users" do scope = policy.apply_scope(Workshop.all, type: :active_record_relation) - expect(scope.to_sql).to include('`workshops`.`visitor_featured` = TRUE') + expect(scope.to_sql).to include('`workshops`.`public_featured` = TRUE') end end end diff --git a/spec/policies/event_policy_spec.rb b/spec/policies/event_policy_spec.rb new file mode 100644 index 000000000..45a108116 --- /dev/null +++ b/spec/policies/event_policy_spec.rb @@ -0,0 +1,164 @@ +require "rails_helper" + +RSpec.describe EventPolicy, type: :policy do + let(:admin_user) { build_stubbed :user, super_user: true } + let(:regular_user) { build_stubbed :user, super_user: false } + let(:published_event) { build_stubbed :event, inactive: false } + let(:public_event) { build_stubbed :event, inactive: false, public: true } + let(:unpublished_event) { build_stubbed :event, inactive: true } + let(:open_registration_event) { build_stubbed :event, inactive: false, registration_close_date: 1.day.from_now } + let(:closed_registration_event) { build_stubbed :event, inactive: false, registration_close_date: 1.day.ago } + + def policy_for(record: nil, user:) + described_class.new(record, user: user) + end + + describe "#index?" do + context "with admin user" do + subject { policy_for(user: admin_user) } + + it { is_expected.to be_allowed_to(:index?) } + end + + context "with regular user" do + subject { policy_for(user: regular_user) } + + it { is_expected.to be_allowed_to(:index?) } + end + + context "with no user" do + subject { policy_for(user: nil) } + + it { is_expected.to be_allowed_to(:index?) } + end + end + + describe "#show?" do + context "when event is visible" do + context "with admin user" do + subject { policy_for(record: published_event, user: admin_user) } + + it { is_expected.to be_allowed_to(:show?) } + end + + context "with regular user" do + subject { policy_for(record: published_event, user: regular_user) } + + it { is_expected.to be_allowed_to(:show?) } + end + + context "with no user" do + subject { policy_for(record: public_event, user: nil) } + + it { is_expected.to be_allowed_to(:show?) } + end + end + + context "when event is not visible" do + context "with admin user" do + subject { policy_for(record: unpublished_event, user: admin_user) } + + it { is_expected.to be_allowed_to(:show?) } + end + + context "with regular user" do + subject { policy_for(record: unpublished_event, user: regular_user) } + + it { is_expected.not_to be_allowed_to(:show?) } + end + + context "with no user" do + subject { policy_for(record: unpublished_event, user: nil) } + + it { is_expected.not_to be_allowed_to(:show?) } + end + end + end + + describe "#manage?" do + context "with admin user" do + subject { policy_for(user: admin_user) } + + it { is_expected.to be_allowed_to(:manage?) } + end + + context "with regular user" do + subject { policy_for(user: regular_user) } + + it { is_expected.not_to be_allowed_to(:manage?) } + end + + context "with no user" do + subject { policy_for(user: nil) } + + it { is_expected.not_to be_allowed_to(:manage?) } + end + end + + describe "aliases to :manage?" do + let(:policy) { policy_for(user: admin_user) } + + describe "#new?" do + it "is an alias of :manage? authorization rule" do + expect(:new?).to be_an_alias_of(policy, :manage?) + end + end + + describe "#create?" do + it "is an alias of :manage? authorization rule" do + expect(:create?).to be_an_alias_of(policy, :manage?) + end + end + + describe "#edit?" do + it "is an alias of :manage? authorization rule" do + expect(:edit?).to be_an_alias_of(policy, :manage?) + end + end + + describe "#update?" do + it "is an alias of :manage? authorization rule" do + expect(:update?).to be_an_alias_of(policy, :manage?) + end + end + + describe "#destroy?" do + it "is an alias of :manage? authorization rule" do + expect(:destroy?).to be_an_alias_of(policy, :manage?) + end + end + end + + describe "relation_scope" do + context "with admin user" do + let(:policy) { policy_for(record: Event, user: admin_user) } + + it "returns all events" do + scope = policy.apply_scope(Event.all, type: :active_record_relation) + expect(scope).to eq(Event.all) + end + end + + context "with regular user" do + let(:policy) { policy_for(record: Event, user: regular_user) } + + it "returns only visible events with open registration" do + scope = policy.apply_scope(Event.all, type: :active_record_relation) + expect(scope.to_sql).to include('`events`.`inactive` = FALSE') + expect(scope.to_sql).to include('registration_close_date IS NULL OR registration_close_date >=') + expect(scope.to_sql).to include('LEFT OUTER JOIN `event_registrations`') + end + end + + context "with no user" do + let(:policy) { policy_for(record: Event, user: nil) } + + it "returns only visible events with open registration" do + scope = policy.apply_scope(Event.all, type: :active_record_relation) + expect(scope.to_sql).to include('`events`.`inactive` = FALSE') + expect(scope.to_sql).to include('registration_close_date IS NULL OR registration_close_date >=') + expect(scope.to_sql).not_to include('LEFT OUTER JOIN `registrants`') + end + end + end +end diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index c9d4ecce1..5a16a19bf 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -8,7 +8,7 @@ "start_date": 1.day.from_now, "end_date": 2.days.from_now, "registration_close_date": 3.days.ago, - "publicly_visible": true + "inactive": false } } @@ -49,9 +49,9 @@ describe "GET /new" do it "renders a successful response" do - sign_in user + sign_in admin allow_any_instance_of(ApplicationController). - to receive(:current_user).and_return(user) + to receive(:current_user).and_return(admin) get new_event_url @@ -79,7 +79,7 @@ get edit_event_url(event) expect(response).to have_http_status(:found) # 302 redirect - expect(response).to redirect_to(events_path) + expect(response).to redirect_to(root_path) end end end @@ -87,27 +87,27 @@ describe "POST /create" do context "with valid parameters" do it "creates a new Event" do - sign_in user + sign_in admin expect { post events_url, params: { event: valid_attributes } }.to change(Event, :count).by(1) end it "redirects to the events index" do - sign_in user + sign_in admin post events_url, params: { event: valid_attributes } expect(response).to redirect_to(events_url) end it "displays notice if present" do - sign_in user + sign_in admin post events_url, params: { event: { title: "sample title", description: "sample description", start_date: 1.day.from_now, end_date: 2.days.from_now, registration_close_date: 3.days.ago, - publicly_visible: true + inactive: false } } follow_redirect! # flash shows after redirect @@ -117,14 +117,14 @@ context "with invalid parameters" do it "does not create a new Event" do - sign_in user + sign_in admin expect { post events_url, params: { event: invalid_attributes } }.to change(Event, :count).by(0) end it "renders a response with validation errors (i.e. to display the 'new' template)" do - sign_in user + sign_in admin post events_url, params: { event: invalid_attributes } expect(response).to have_http_status(:unprocessable_content) end @@ -141,7 +141,7 @@ context "when signed in as admin" do it "updates the requested event" do - sign_in user + sign_in admin allow_any_instance_of(ApplicationController). to receive(:current_user).and_return(admin) patch event_url(event), params: { event: new_attributes } @@ -151,7 +151,7 @@ end it "redirects to the events index" do - sign_in user + sign_in admin allow_any_instance_of(ApplicationController). to receive(:current_user).and_return(admin) @@ -169,7 +169,7 @@ patch event_url(event), params: { event: new_attributes } expect(response).to have_http_status(:found) - expect(response).to redirect_to(events_path) + expect(response).to redirect_to(root_path) end end end @@ -197,7 +197,7 @@ end it "redirects to the events list" do - sign_in user + sign_in admin allow_any_instance_of(ApplicationController). to receive(:current_user).and_return(admin) delete event_url(event) diff --git a/spec/system/facilitator_adds_event_to_calendar_test.rb b/spec/system/facilitator_adds_event_to_calendar_test.rb index b507f4dd2..88a25ac8f 100644 --- a/spec/system/facilitator_adds_event_to_calendar_test.rb +++ b/spec/system/facilitator_adds_event_to_calendar_test.rb @@ -12,7 +12,7 @@ start_date: 1.day.from_now, end_date: 2.days.from_now, registration_close_date: 1.day.from_now, - publicly_visible: true, + inactive: false, featured: true ) create(:event, @@ -20,7 +20,7 @@ start_date: 2.days.from_now, end_date: 3.days.from_now, registration_close_date: 1.day.from_now, - publicly_visible: true, + inactive: false, featured: true, created_by: user ) diff --git a/spec/system/facilitator_registers_for_event_test.rb b/spec/system/facilitator_registers_for_event_test.rb index 8193553e4..47f519481 100644 --- a/spec/system/facilitator_registers_for_event_test.rb +++ b/spec/system/facilitator_registers_for_event_test.rb @@ -12,7 +12,7 @@ start_date: 1.day.from_now, end_date: 2.days.from_now, registration_close_date: 1.day.from_now, - publicly_visible: true, +inactive: false, featured: true ) create(:event, @@ -20,7 +20,7 @@ start_date: 2.days.from_now, end_date: 3.days.from_now, registration_close_date: 1.day.from_now, - publicly_visible: true, + inactive: false, featured: true, created_by: user ) diff --git a/spec/system/workshops_spec.rb b/spec/system/workshops_spec.rb index 88dbf36c8..56c66e571 100644 --- a/spec/system/workshops_spec.rb +++ b/spec/system/workshops_spec.rb @@ -69,8 +69,6 @@ visit new_workshop_path(windows_type_id: adult_window.id) - save_and_open_page - fill_in "workshop_title", with: 'My New Workshop' select adult_window.short_name, from: 'workshop_windows_type_id' find('#body-button').click @@ -78,8 +76,6 @@ click_on 'Submit' - save_and_open_page - # expect(Workshop.last.title).to eq('My New Workshop') expect(page).to have_content('My New Workshop') # expect(page).to have_content('Learn something new') diff --git a/spec/views/events/_form.html.erb_spec.rb b/spec/views/events/_form.html.erb_spec.rb index 8aa48972c..b03198005 100644 --- a/spec/views/events/_form.html.erb_spec.rb +++ b/spec/views/events/_form.html.erb_spec.rb @@ -6,6 +6,7 @@ before do assign(:event, event) allow(view).to receive(:current_user).and_return(build_stubbed(:user, super_user: true)) + allow(view).to receive(:allowed_to?).with(:manage?, event).and_return(true) end it "renders all form fields" do @@ -17,7 +18,7 @@ expect(rendered).to have_selector("input[type='datetime-local'][name='event[start_date]']") expect(rendered).to have_selector("input[type='datetime-local'][name='event[end_date]']") expect(rendered).to have_selector("input[type='datetime-local'][name='event[registration_close_date]']") - expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]']") end it "renders all form labels" do @@ -29,7 +30,7 @@ expect(rendered).to have_selector("label", text: "Start time") expect(rendered).to have_selector("label", text: "End time") expect(rendered).to have_selector("label", text: "Registration close time") - expect(rendered).to have_selector("label", text: "Publicly visible") + expect(rendered).to have_selector("label", text: "Hidden?") end it "renders submit button" do @@ -46,7 +47,7 @@ start_date: DateTime.new(2024, 1, 15, 10, 0), end_date: DateTime.new(2024, 1, 15, 16, 0), registration_close_date: DateTime.new(2024, 1, 10, 23, 59), - publicly_visible: true) + inactive: false) end it "populates form fields with existing data" do @@ -54,7 +55,8 @@ expect(rendered).to have_field("event[title]", with: "Existing Event") expect(rendered).to have_selector("textarea", text: "Existing description") - expect(rendered).to have_selector("input[type='checkbox'][checked='checked']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]']") + expect(rendered).not_to have_selector("input[type='checkbox'][checked='checked']") end it "populates datetime fields with properly formatted values" do @@ -93,24 +95,24 @@ end end - context "when publicly_visible is false" do - let(:event) { create(:event, publicly_visible: false) } + context "when inactive is true" do + let(:event) { create(:event, inactive: true) } - it "renders unchecked checkbox" do + it "renders checked checkbox" do render - expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") - expect(rendered).not_to have_selector("input[type='checkbox'][checked='checked']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]'][checked='checked']") end end - context "when publicly_visible is true" do - let(:event) { create(:event, publicly_visible: true) } + context "when inactive is false" do + let(:event) { create(:event, inactive: false) } - it "renders checked checkbox" do + it "renders unchecked checkbox" do render - expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]'][checked='checked']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]']") + expect(rendered).not_to have_selector("input[type='checkbox'][checked='checked']") end end end diff --git a/spec/views/events/edit.html.erb_spec.rb b/spec/views/events/edit.html.erb_spec.rb index b226376bd..b823f5eaa 100644 --- a/spec/views/events/edit.html.erb_spec.rb +++ b/spec/views/events/edit.html.erb_spec.rb @@ -8,12 +8,13 @@ start_date: DateTime.new(2024, 1, 15, 10, 0), end_date: DateTime.new(2024, 1, 15, 16, 0), registration_close_date: DateTime.new(2024, 1, 10, 23, 59), - publicly_visible: true) + inactive: false) end before do assign(:event, event) allow(view).to receive(:current_user).and_return(build_stubbed(:user, super_user: true)) + allow(view).to receive(:allowed_to?).with(:manage?, event).and_return(true) end it "renders the editing event heading" do @@ -28,7 +29,8 @@ expect(rendered).to have_selector("form") expect(rendered).to have_field("event[title]", with: "Original Title") expect(rendered).to have_selector("textarea[name='event[description]']", text: "Original description") - expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]'][checked='checked']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]']") + expect(rendered).not_to have_selector("input[type='checkbox'][checked='checked']") end it "renders action links" do @@ -58,18 +60,17 @@ end end - context "when event is not publicly visible" do + context "when event is inactive" do let(:event) do create(:event, title: "Private Event", - publicly_visible: false) + inactive: true) end - it "renders unchecked checkbox" do + it "renders checked checkbox" do render - expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") - expect(rendered).not_to have_selector("input[type='checkbox'][name='event[publicly_visible]'][checked='checked']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]'][checked='checked']") end end end diff --git a/spec/views/events/index.html.erb_spec.rb b/spec/views/events/index.html.erb_spec.rb index 1a80980c1..7d3d56466 100644 --- a/spec/views/events/index.html.erb_spec.rb +++ b/spec/views/events/index.html.erb_spec.rb @@ -5,26 +5,29 @@ let(:event_closed) { create(:event, title: "Event 1", start_date: 1.day.from_now, end_date: 2.days.from_now, - publicly_visible: true, + inactive: false, registration_close_date: -3.days.from_now) } let(:event_open) { create(:event, title: "Event 2", start_date: 3.days.from_now, end_date: 4.days.from_now, registration_close_date: 5.days.from_now, - publicly_visible: true) + inactive: false) } let(:event_open_2) { create(:event, title: "Event 2", start_date: 3.days.from_now, end_date: 4.days.from_now, registration_close_date: nil, - publicly_visible: true) + inactive: false) } let(:events) { [ event_open, event_open ] } before do assign(:events, events) allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:allowed_to?).with(:new?, Event).and_return(true) + allow(view).to receive(:allowed_to?).with(:edit?, Event).and_return(true) + allow(view).to receive(:allowed_to?).with(:update?, Bookmark).and_return(true) end it "renders each event with checkbox and details" do diff --git a/spec/views/events/new.html.erb_spec.rb b/spec/views/events/new.html.erb_spec.rb index 5cd6a2922..79d2a6697 100644 --- a/spec/views/events/new.html.erb_spec.rb +++ b/spec/views/events/new.html.erb_spec.rb @@ -6,6 +6,7 @@ before do assign(:event, event) allow(view).to receive(:current_user).and_return(build_stubbed(:user, super_user: true)) + allow(view).to receive(:allowed_to?).with(:manage?, event).and_return(true) end it "renders the new event heading" do @@ -23,7 +24,7 @@ expect(rendered).to have_selector("input[type='datetime-local'][name='event[start_date]']") expect(rendered).to have_selector("input[type='datetime-local'][name='event[end_date]']") expect(rendered).to have_selector("input[type='datetime-local'][name='event[registration_close_date]']") - expect(rendered).to have_selector("input[type='checkbox'][name='event[publicly_visible]']") + expect(rendered).to have_selector("input[type='checkbox'][name='event[inactive]']") end it "renders the Cancel link" do diff --git a/spec/views/events/show.html.erb_spec.rb b/spec/views/events/show.html.erb_spec.rb index 261aada5d..b992473fa 100644 --- a/spec/views/events/show.html.erb_spec.rb +++ b/spec/views/events/show.html.erb_spec.rb @@ -13,6 +13,7 @@ before do assign(:event, event.decorate) allow(view).to receive(:current_user).and_return(build_stubbed(:user, super_user: true)) + allow(view).to receive(:allowed_to?).and_return(true) end it "renders the event title" do