Skip to content

Commit aba8459

Browse files
committed
feat: add project types
1 parent 7700872 commit aba8459

24 files changed

Lines changed: 364 additions & 57 deletions

File tree

app/assets/stylesheets/components/_action_button.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
&--disabled {
142142
opacity: 0.5;
143143
cursor: not-allowed;
144+
pointer-events: none;
144145
}
145146

146147
// Natively-disabled controls block all pointer events. Soft-disabled

app/assets/stylesheets/pages/certification/ships/_index.scss

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,88 @@
604604
margin-top: var(--space-xs);
605605
}
606606

607+
// --- Type breakdown + filter chips ------------------------------------
608+
&__types {
609+
padding-top: var(--space-m);
610+
border-top: 1px solid var(--card-border);
611+
}
612+
613+
&__type-chips {
614+
display: flex;
615+
flex-wrap: wrap;
616+
align-items: center;
617+
gap: var(--space-xs);
618+
margin-top: var(--space-xs);
619+
}
620+
621+
&__type-chip {
622+
display: inline-flex;
623+
align-items: center;
624+
gap: 5px;
625+
padding: 2px 10px;
626+
border-radius: 999px;
627+
border: 1px solid var(--card-border);
628+
background: transparent;
629+
color: var(--muted);
630+
font-size: 0.78rem;
631+
text-decoration: none;
632+
white-space: nowrap;
633+
transition:
634+
border-color 150ms ease,
635+
color 150ms ease,
636+
background 150ms ease;
637+
638+
&:hover,
639+
&:focus-visible {
640+
border-color: var(--accent);
641+
color: var(--color-space-text);
642+
}
643+
644+
&.is-active {
645+
background: var(--accent);
646+
border-color: var(--accent);
647+
color: var(--color-set-1-bg, #1a1633);
648+
}
649+
}
650+
651+
&__type-chip-count {
652+
font-weight: 700;
653+
font-variant-numeric: tabular-nums;
654+
opacity: 0.85;
655+
}
656+
657+
&__type-clear {
658+
color: var(--muted);
659+
font-size: 0.78rem;
660+
text-decoration: none;
661+
margin-left: var(--space-xs);
662+
663+
&:hover,
664+
&:focus-visible {
665+
color: var(--accent);
666+
text-decoration: underline;
667+
}
668+
}
669+
670+
// Inline type tag in table rows and mobile cards
671+
&__type-tag {
672+
display: inline-flex;
673+
align-items: center;
674+
padding: 1px 7px;
675+
border-radius: 999px;
676+
font-size: 0.7rem;
677+
font-weight: 600;
678+
letter-spacing: 0.02em;
679+
background: rgba(255, 255, 255, 0.12);
680+
color: rgba(255, 255, 255, 0.9);
681+
white-space: nowrap;
682+
683+
&--own {
684+
background: rgba(251, 146, 60, 0.2);
685+
color: #fb923c;
686+
}
687+
}
688+
607689
// --- Empty state -------------------------------------------------------
608690
&__empty {
609691
padding: var(--space-xl);

app/controllers/admin/certification/ships_controller.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,30 @@ class Admin::Certification::ShipsController < Admin::Certification::ApplicationC
66
def index
77
authorize ::Certification::Ship
88

9-
@status = params[:status].presence_in(%w[pending approved returned all]) || "pending"
10-
@sort = params[:sort] == "newest" ? "newest" : "oldest"
11-
@search = params[:search].to_s.strip
12-
@from = parse_date(params[:from])
13-
@to = parse_date(params[:to])
9+
@status = params[:status].presence_in(%w[pending approved returned all]) || "pending"
10+
@sort = params[:sort] == "newest" ? "newest" : "oldest"
11+
@search = params[:search].to_s.strip
12+
@from = parse_date(params[:from])
13+
@to = parse_date(params[:to])
14+
@project_type = params[:project_type].presence
1415

1516
scope = policy_scope(::Certification::Ship)
16-
.includes(:reviewer, project: { memberships: :user })
1717
scope = scope.where(status: @status) unless @status == "all"
1818
scope = scope.where("certification_ship_reviews.created_at >= ?", @from.beginning_of_day) if @from
1919
scope = scope.where("certification_ship_reviews.created_at <= ?", @to.end_of_day) if @to
2020
scope = apply_search(scope) if @search.present?
2121

22+
@type_counts = scope.joins(:project).group("projects.project_type").count
23+
24+
scope = scope.by_project_type(@project_type) if @project_type.present?
25+
2226
@pagy, @ships = pagy(:offset,
23-
scope.order(created_at: @sort == "newest" ? :desc : :asc),
27+
scope.includes(:reviewer, project: { memberships: :user })
28+
.order(created_at: @sort == "newest" ? :desc : :asc),
2429
limit: 25)
2530

31+
@own_project_ids = current_user.memberships.pluck(:project_id).to_set
32+
2633
@stats = ::Certification::Ship.dashboard_stats
2734
@lb_period = params[:lb].presence_in(%w[daily weekly alltime]) || "daily"
2835
@leaderboards = {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
class OneTime::BackfillProjectTypeJob < ApplicationJob
4+
queue_as :literally_whenever
5+
6+
def scope
7+
Project.where(project_type: nil, deleted_at: nil).where.not(shipped_at: nil)
8+
end
9+
10+
def perform
11+
count = 0
12+
13+
scope.find_each do |project|
14+
Project::TypeCheckJob.perform_later(project)
15+
count += 1
16+
end
17+
18+
Rails.logger.info "[OneTime::BackfillProjectType] Enqueued #{count} type-check jobs"
19+
end
20+
end

app/jobs/project/type_check_job.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
class Project::TypeCheckJob < ApplicationJob
4+
queue_as :default
5+
6+
discard_on ActiveJob::DeserializationError
7+
retry_on StandardError, wait: :polynomially_longer, attempts: 3
8+
9+
def perform(project)
10+
result = SwAi::ProjectTypeService.new(project).call
11+
project.update_column(:project_type, result.type) if result.ok && result.type.present?
12+
end
13+
end

app/models/certification/ship.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ class Ship < ApplicationRecord
6262
.where.not(project_id: user.memberships.select(:project_id))
6363
}
6464

65+
scope :by_project_type, ->(type) {
66+
type == "unclassified" \
67+
? joins(:project).where(projects: { project_type: nil })
68+
: joins(:project).where(projects: { project_type: type })
69+
}
70+
6571
def self.available_for(user)
6672
super.merge(for_reviewer(user))
6773
end

app/models/gorse/post_payload.rb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,14 @@ def hidden?
5252
attr_reader :post
5353

5454
def categories
55-
[ "feed", post_type, project_categories ].flatten.compact_blank.uniq
55+
[ "feed", post_type, post.project&.project_type ].compact_blank.uniq
5656
end
5757

5858
def labels
5959
Gorse::Labels.cast(
6060
type: post_type,
6161
project_id: post.project_id,
6262
author_id: post.user_id,
63-
project_categories: project_categories,
6463
project_type: post.project&.project_type,
6564
has_media: has_media?,
6665
certification_status: ship_certification_status
@@ -71,10 +70,6 @@ def post_type
7170
post.postable_type.to_s.demodulize.underscore
7271
end
7372

74-
def project_categories
75-
Array(post.project&.project_categories)
76-
end
77-
7873
def comment
7974
if post.postable.respond_to?(:body)
8075
post.postable.body.to_s.truncate(240)

app/models/gorse/project_payload.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,11 @@ def hidden?
3737
attr_reader :project
3838

3939
def categories
40-
[ "project", project.project_type, project.project_categories ].flatten.compact_blank.uniq
40+
[ "project", project.project_type ].compact_blank.uniq
4141
end
4242

4343
def labels
4444
Gorse::Labels.cast(
45-
project_categories: project.project_categories,
4645
project_type: project.project_type,
4746
ship_status: project.ship_status,
4847
tutorial: project.tutorial?,

app/models/post/ship_event.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class Post::ShipEvent < ApplicationRecord
7474
scope :legacy_voting_scale, -> { where(voting_scale_version: LEGACY_VOTING_SCALE_VERSION) }
7575

7676
after_commit :decrement_user_vote_balance, on: :create
77+
after_commit :schedule_type_check, on: :create
7778

7879
validates :body, presence: { message: "Update message can't be blank" }
7980
validates :body, length: { maximum: BODY_MAX_LENGTH }, on: :create
@@ -143,6 +144,11 @@ def decrement_user_vote_balance
143144
post.user.increment!(:vote_balance, -VOTE_COST_PER_SHIP)
144145
end
145146

147+
def schedule_type_check
148+
project = post&.project
149+
Project::TypeCheckJob.perform_later(project) if project && project.project_type.nil?
150+
end
151+
146152
# Drives the Mission::Submission state machine off ship cert transitions.
147153
# See docs/missions-design.md "Certification interaction" for the spec.
148154
def sync_mission_submission_status

app/models/project.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# marked_fire_at :datetime
1313
# memberships_count :integer default(0), not null
1414
# nominated_fire_at :datetime
15-
# project_categories :string default([]), is an Array
1615
# project_type :string
1716
# readme_url :text
1817
# repo_url :text
@@ -160,16 +159,7 @@ def shipped_to_mission?(mission)
160159
content_type: { in: ACCEPTED_CONTENT_TYPES, spoofing_protection: true },
161160
size: { less_than: MAX_BANNER_SIZE, message: "is too large (max 10 MB)" },
162161
processable_file: true
163-
validate :validate_project_categories
164162

165-
def validate_project_categories
166-
return if project_categories.blank?
167-
168-
invalid_types = project_categories - AVAILABLE_CATEGORIES
169-
if invalid_types.any?
170-
errors.add(:project_categories, "contains invalid types: #{invalid_types.join(', ')}")
171-
end
172-
end
173163

174164
def validate_repo_cloneable
175165
return false if repo_url.blank?

0 commit comments

Comments
 (0)