Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/components/sidebar_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def nav_items
end

# Guardians of integrity only (not admins): YSWS certification review queue.
if signed_in? && user.guardian_of_integrity?
# Both subcategories — regular and hardware GOI — get the link.
if signed_in? && user.guardian_of_integrity_any?
items << { slug: "guard", label: "Lets go GOI", path: helpers.admin_certification_ysws_reviews_path, icon: "eye" }
end

Expand Down
4 changes: 3 additions & 1 deletion app/controllers/admin/certification/ysws_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ def index
@sort = params[:sort].presence_in(%w[length todo])
@dir = params[:dir] == "asc" ? "asc" : "desc"

scope = ::Certification::Ysws.where(reviewed_at: nil, returned_at: nil)
# policy_scope splits the queue by GOI subcategory: a regular GOI sees only
# software reviews, a Hardware GOI only hardware reviews, admins see all.
scope = policy_scope(::Certification::Ysws).where(reviewed_at: nil, returned_at: nil)

# Type filter options are whatever project types are actually present in the
# pending queue (plus an "unclassified" bucket) — never hardcoded.
Expand Down
6 changes: 6 additions & 0 deletions app/models/certification/ysws.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ class Ysws < ApplicationRecord
: joins(:project).where(projects: { project_type: type })
}

# Hardware vs. software split for the GOI subcategories. "Hardware" is the
# canonical Project#hardware? marker (hardware_stage present), NOT the
# AI-classified project_type. Consumed by Admin::Certification::YswsPolicy::Scope.
scope :hardware, -> { joins(:project).where.not(projects: { hardware_stage: nil }) }
scope :non_hardware, -> { joins(:project).where(projects: { hardware_stage: nil }) }

# Count of still-pending child devlog reviews. Available only on records
# loaded through .with_todo_devlog_count.
def todo_devlog_count
Expand Down
3 changes: 3 additions & 0 deletions app/models/lookout_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@
# token :string not null
# created_at :datetime not null
# updated_at :datetime not null
# devlog_id :bigint
# project_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_lookout_sessions_on_devlog_id (devlog_id)
# index_lookout_sessions_on_project_id (project_id)
# index_lookout_sessions_on_project_id_and_status (project_id,status)
# index_lookout_sessions_on_token (token) UNIQUE
# index_lookout_sessions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (devlog_id => post_devlogs.id)
# fk_rails_... (project_id => projects.id)
# fk_rails_... (user_id => users.id)
#
Expand Down
2 changes: 1 addition & 1 deletion app/models/mission/guide_variant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# Indexes
#
# index_mission_guide_variants_on_mission_id (mission_id)
# index_mission_guide_variants_unique_language (mission_id,language) UNIQUE
# index_mission_guide_variants_unique_language (mission_id, lower((language)::text)) UNIQUE
#
# Foreign Keys
#
Expand Down
2 changes: 1 addition & 1 deletion app/models/mission/step_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Indexes
#
# index_mission_step_bodies_on_mission_step_id (mission_step_id)
# index_mission_step_bodies_unique_language (mission_step_id,language) UNIQUE
# index_mission_step_bodies_unique_language (mission_step_id, lower((language)::text)) UNIQUE
#
# Foreign Keys
#
Expand Down
2 changes: 0 additions & 2 deletions app/models/post/ship_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
# id :bigint not null, primary key
# body :string
# certification_status :string default("pending")
# comments_count :integer default(0), not null
# feedback_reason :text
# feedback_video_url :string
# hours_at_payout :float
# hours_at_ship :float
# likes_count :integer default(0), not null
# multiplier :float
# originality_median :decimal(5, 2)
# originality_percentile :decimal(5, 2)
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
# index_users_on_approx_balance (approx_balance)
# index_users_on_approx_total_earned (approx_total_earned)
# index_users_on_email (email)
# index_users_on_guest_email (guest_email)
# index_users_on_lower_display_name_unique (lower((display_name)::text)) UNIQUE WHERE ((display_name IS NOT NULL) AND ((display_name)::text <> ''::text))
# index_users_on_lower_email_unique (lower((email)::text)) UNIQUE WHERE ((email IS NOT NULL) AND ((email)::text <> ''::text))
# index_users_on_onboarded_at (onboarded_at)
Expand Down
3 changes: 2 additions & 1 deletion app/models/user/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class User
new(6, :helper, "Support team with read-only access to users (no PII), projects, and shop orders"),
new(7, :shop_manager, "Can create/edit draft shop items and view orders without PII"),
new(8, :mission_reviewer, "Can review submissions for any mission across the platform"),
new(9, :raffle_admin, "Can manage the referral raffle: weeks, draws, participants, and referrals")
new(9, :raffle_admin, "Can manage the referral raffle: weeks, draws, participants, and referrals"),
new(10, :hardware_guardian_of_integrity, "Can approve/reject hardware projects for YSWS DB (GOI restricted to hardware)")
].freeze

self::SLUGGED = self::ALL.index_by(&:name).freeze
Expand Down
29 changes: 28 additions & 1 deletion app/models/user/roles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,34 @@ def admin? = has_role?(:admin) || has_role?(:super_admin)

def can_review? = admin? || has_role?(:project_certifier)

def can_nominate_super_star? = can_review? || has_role?(:guardian_of_integrity)
# Either kind of Guardian of Integrity: the regular role or the hardware
# subcategory. Excludes plain admins (who can see everything but aren't GOIs).
def guardian_of_integrity_any? = has_role?(:guardian_of_integrity) || has_role?(:hardware_guardian_of_integrity)

# Who may reach the YSWS review queue: admins and both GOI subcategories. The
# queue is then split by category — regular GOI sees software, hardware GOI
# sees hardware — in Admin::Certification::YswsPolicy::Scope.
def can_review_ysws? = admin? || guardian_of_integrity_any?

# Whether this reviewer may act on a project of the given category. Admins see
# everything; a Hardware GOI is confined to hardware projects, a regular GOI
# to software. Keeps the queue split (YswsPolicy::Scope) and every member
# action (YSWS review, devlog review, devlog commits) consistent. A nil
# project can't be categorised, so it's allowed (the controller handles it).
def can_review_project_category?(project)
return true if admin?
return true if project.nil?

if has_role?(:hardware_guardian_of_integrity)
project.hardware?
elsif has_role?(:guardian_of_integrity)
!project.hardware?
else
false
end
end

def can_nominate_super_star? = can_review? || guardian_of_integrity_any?

def can_see_deleted_devlogs? = admin? || has_role?(:fraud_dept)

Expand Down
5 changes: 4 additions & 1 deletion app/policies/admin/certification/devlog_commits_policy.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
class Admin::Certification::DevlogCommitsPolicy < ApplicationPolicy
def index?
user.admin? || user.has_role?(:guardian_of_integrity)
return false unless user&.can_review_ysws?
# record is a Post::Devlog; confine reviewers to their own category so a
# Hardware GOI can't pull commits for a software project (and vice versa).
user.can_review_project_category?(record.try(:post)&.project)
end
end
6 changes: 5 additions & 1 deletion app/policies/admin/certification/devlog_policy.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
class Admin::Certification::DevlogPolicy < ApplicationPolicy
def update?
user&.admin? || user&.has_role?(:guardian_of_integrity)
return false unless user&.can_review_ysws?
# record is a Certification::Devlog (devlog review); confine reviewers to
# their own category so a Hardware GOI can't edit a software project's
# devlog verdicts (and vice versa).
user.can_review_project_category?(record.try(:ysws_review)&.project)
end
end
27 changes: 23 additions & 4 deletions app/policies/admin/certification/ysws_policy.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
class Admin::Certification::YswsPolicy < ApplicationPolicy
def index?
user.admin? || user.has_role?(:guardian_of_integrity)
user.can_review_ysws?
end

def show?
index?
user.can_review_ysws? && user.can_review_project_category?(record.try(:project))
end

def dashboard?
index?
end

def update?
index?
show?
end

def report_fraud?
index?
show?
end

# Splits the review queue by reviewer subcategory: a regular Guardian of
# Integrity sees only software reviews, a Hardware GOI only hardware reviews,
# and admins (plus the dev nil bypass) see everything. "Hardware" is the
# canonical Project#hardware? marker (hardware_stage present), not the
# AI-classified project_type.
class Scope < ApplicationPolicy::Scope
def resolve
return scope.all || user.admin?

if user.hardware_guardian_of_integrity?
scope.hardware
elsif user.guardian_of_integrity?
scope.non_hardware
else
scope.none
end
end
end
end
2 changes: 1 addition & 1 deletion app/policies/admin_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def access_ship_review?
end

def access_ysws_review?
user.admin? || user.has_role?(:guardian_of_integrity)
user.can_review_ysws?
end

def access_blazer?
Expand Down
1 change: 1 addition & 0 deletions app/views/admin/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
{ name: 'project_certifier', disabled: !current_user&.admin? },
{ name: 'guardian_of_integrity', disabled: !current_user&.admin? },
{ name: 'mission_reviewer', disabled: !current_user&.admin? },
{ name: 'hardware_guardian_of_integrity', disabled: !current_user&.admin? },
{ name: 'helper', disabled: !current_user&.admin? },
{ name: 'shop_manager', disabled: !current_user&.admin? },
{ name: 'admin', disabled: !current_user&.super_admin? },
Expand Down
2 changes: 0 additions & 2 deletions test/fixtures/post/ship_events.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
# id :bigint not null, primary key
# body :string
# certification_status :string default("pending")
# comments_count :integer default(0), not null
# feedback_reason :text
# feedback_video_url :string
# hours_at_payout :float
# hours_at_ship :float
# likes_count :integer default(0), not null
# multiplier :float
# originality_median :decimal(5, 2)
# originality_percentile :decimal(5, 2)
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
# index_users_on_approx_balance (approx_balance)
# index_users_on_approx_total_earned (approx_total_earned)
# index_users_on_email (email)
# index_users_on_guest_email (guest_email)
# index_users_on_lower_display_name_unique (lower((display_name)::text)) UNIQUE WHERE ((display_name IS NOT NULL) AND ((display_name)::text <> ''::text))
# index_users_on_lower_email_unique (lower((email)::text)) UNIQUE WHERE ((email IS NOT NULL) AND ((email)::text <> ''::text))
# index_users_on_onboarded_at (onboarded_at)
Expand Down
3 changes: 3 additions & 0 deletions test/models/lookout_session_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@
# token :string not null
# created_at :datetime not null
# updated_at :datetime not null
# devlog_id :bigint
# project_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_lookout_sessions_on_devlog_id (devlog_id)
# index_lookout_sessions_on_project_id (project_id)
# index_lookout_sessions_on_project_id_and_status (project_id,status)
# index_lookout_sessions_on_token (token) UNIQUE
# index_lookout_sessions_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (devlog_id => post_devlogs.id)
# fk_rails_... (project_id => projects.id)
# fk_rails_... (user_id => users.id)
#
Expand Down
2 changes: 0 additions & 2 deletions test/models/post/ship_event_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
# id :bigint not null, primary key
# body :string
# certification_status :string default("pending")
# comments_count :integer default(0), not null
# feedback_reason :text
# feedback_video_url :string
# hours_at_payout :float
# hours_at_ship :float
# likes_count :integer default(0), not null
# multiplier :float
# originality_median :decimal(5, 2)
# originality_percentile :decimal(5, 2)
Expand Down
1 change: 1 addition & 0 deletions test/models/user_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
# index_users_on_approx_balance (approx_balance)
# index_users_on_approx_total_earned (approx_total_earned)
# index_users_on_email (email)
# index_users_on_guest_email (guest_email)
# index_users_on_lower_display_name_unique (lower((display_name)::text)) UNIQUE WHERE ((display_name IS NOT NULL) AND ((display_name)::text <> ''::text))
# index_users_on_lower_email_unique (lower((email)::text)) UNIQUE WHERE ((email IS NOT NULL) AND ((email)::text <> ''::text))
# index_users_on_onboarded_at (onboarded_at)
Expand Down
Loading