Skip to content

Commit d151815

Browse files
authored
Merge pull request #360 from Nullskulls/feat/project-types
feat(ship): add project types
2 parents 1cf58c8 + 131724a commit d151815

19 files changed

Lines changed: 254 additions & 62 deletions

File tree

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
border-radius: 12px;
6666
}
6767

68-
&__label {
68+
&__label,
69+
&__filters label {
6970
display: block;
7071
font-size: 0.75rem;
7172
text-transform: uppercase;
@@ -306,15 +307,6 @@
306307
gap: var(--space-m);
307308
padding: var(--space-m) var(--space-l);
308309
margin-bottom: var(--space-l);
309-
310-
label {
311-
display: block;
312-
font-size: 0.75rem;
313-
text-transform: uppercase;
314-
letter-spacing: 0.06em;
315-
color: var(--muted);
316-
margin-bottom: var(--space-xxs);
317-
}
318310
}
319311

320312
&__filter {
@@ -604,6 +596,25 @@
604596
margin-top: var(--space-xs);
605597
}
606598

599+
// Inline type tags in table rows and mobile cards.
600+
&__type-tag {
601+
display: inline-flex;
602+
align-items: center;
603+
padding: 1px 7px;
604+
border-radius: 999px;
605+
font-size: 0.7rem;
606+
font-weight: 600;
607+
letter-spacing: 0.02em;
608+
background: rgba(255, 255, 255, 0.12);
609+
color: rgba(255, 255, 255, 0.9);
610+
white-space: nowrap;
611+
612+
&--own {
613+
background: rgba(255, 176, 122, 0.2);
614+
color: var(--color-brand-orange);
615+
}
616+
}
617+
607618
// --- Empty state -------------------------------------------------------
608619
&__empty {
609620
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
@@ -7,23 +7,30 @@ class Admin::Certification::ShipsController < Admin::Certification::ApplicationC
77
def index
88
authorize ::Certification::Ship
99

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

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

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

32+
@own_project_ids = current_user.memberships.pluck(:project_id).to_set
33+
2734
@stats = ::Certification::Ship.dashboard_stats
2835
@lb_period = params[:lb].presence_in(%w[daily weekly alltime]) || "daily"
2936
@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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
return unless result.ok && result.type.present?
12+
13+
project.update_column(:project_type, result.type)
14+
sync_type_to_gorse_later(project)
15+
end
16+
17+
private
18+
19+
def sync_type_to_gorse_later(project)
20+
project.sync_to_gorse_later
21+
project.posts.of_ship_events.find_each(&:sync_to_gorse_later)
22+
end
23+
end

app/models/certification/ship.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ def owner
103103
.where.not(project_id: user.memberships.select(:project_id))
104104
}
105105

106+
scope :by_project_type, ->(type) {
107+
type == "unclassified" \
108+
? joins(:project).where(projects: { project_type: nil })
109+
: joins(:project).where(projects: { project_type: type })
110+
}
111+
106112
def self.available_for(user)
107113
super.merge(for_reviewer(user))
108114
end

app/models/gorse/post_payload.rb

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

5555
def categories
56-
[ "feed", post_type, project_categories ].flatten.compact_blank.uniq
56+
[ "feed", post_type, post.project&.project_type ].compact_blank.uniq
5757
end
5858

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

75-
def project_categories
76-
Array(post.project&.project_categories)
77-
end
78-
7974
def comment
8075
if post.postable.respond_to?(:body)
8176
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
@@ -168,6 +169,11 @@ def decrement_user_vote_balance
168169
post.user.increment!(:vote_balance, -VOTE_COST_PER_SHIP)
169170
end
170171

172+
def schedule_type_check
173+
project = post&.project
174+
Project::TypeCheckJob.perform_later(project) if project && project.project_type.nil?
175+
end
176+
171177
# Drives the Mission::Submission state machine off ship cert transitions.
172178
# See docs/missions-design.md "Certification interaction" for the spec.
173179
def sync_mission_submission_status

app/models/project.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,23 +256,13 @@ def follow_up_targets_for(user)
256256
# allows nil, but not "").
257257
normalizes :hardware_stage, with: ->(value) { value.presence }
258258
validates :hardware_stage, inclusion: { in: HARDWARE_STAGES }, allow_nil: true
259-
validate :validate_project_categories
260259
validate :hardware_stage_locked_after_funding_request
261260

262261
def hardware_stage_locked_after_funding_request
263262
return unless hardware_stage_changed? && has_any_funding_request?
264263
errors.add(:hardware_stage, "cannot be changed after a funding request has been submitted")
265264
end
266265

267-
def validate_project_categories
268-
return if project_categories.blank?
269-
270-
invalid_types = project_categories - AVAILABLE_CATEGORIES
271-
if invalid_types.any?
272-
errors.add(:project_categories, "contains invalid types: #{invalid_types.join(', ')}")
273-
end
274-
end
275-
276266
def validate_repo_cloneable
277267
return false if repo_url.blank?
278268

app/policies/admin/certification/ship_policy.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def next? = user&.can_review?
1515
class Scope < ApplicationPolicy::Scope
1616
def resolve
1717
return scope.none unless user&.can_review?
18-
scope.for_reviewer(user)
18+
scope.joins(:project).where(projects: { deleted_at: nil })
1919
end
2020
end
2121

0 commit comments

Comments
 (0)