Skip to content

Commit f824794

Browse files
authored
Build MVP for ID generation and admin badge management (#290)
1 parent a7286f9 commit f824794

28 files changed

+1009
-37
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
class BadgeBatchesController < ApplicationController
5+
before_action :require_admin
6+
before_action :set_badge_batch, only: %i[show edit update]
7+
8+
def index
9+
@badge_batches = BadgeBatch.includes(:badges).order(created_at: :desc)
10+
end
11+
12+
def show
13+
@badges = @badge_batch.badges.order(:id)
14+
end
15+
16+
def new
17+
@badge_batch = BadgeBatch.new
18+
end
19+
20+
def create
21+
count = badge_batch_params[:count].to_i
22+
note = badge_batch_params[:note]
23+
24+
badge_batch = BadgeBatch.create!(note: note, count: count)
25+
26+
badge_ids = Badge.generate_random_ids(count)
27+
28+
timestamp = Time.current
29+
badge_records = badge_ids.map do |id|
30+
{id: id, badge_batch_id: badge_batch.id, created_at: timestamp,
31+
updated_at: timestamp}
32+
end
33+
Badge.insert_all(badge_records)
34+
35+
flash[:success] = t("badges.messages.batch_created", count: count)
36+
redirect_to badge_batch_path(badge_batch)
37+
end
38+
39+
def edit
40+
end
41+
42+
def update
43+
if @badge_batch.update(note_param)
44+
flash[:success] = t("badges.messages.batch_updated")
45+
redirect_to badge_batch_path(@badge_batch)
46+
else
47+
render :edit
48+
end
49+
end
50+
51+
def search
52+
query = params[:query]&.strip&.upcase
53+
54+
if query.blank?
55+
redirect_to badge_batches_path
56+
return
57+
end
58+
59+
badge = Badge.find_by(id: query)
60+
61+
if badge
62+
flash[:success] = t("badges.messages.search_success")
63+
redirect_to badge_path(badge)
64+
else
65+
flash[:alert] = t("badges.messages.search_not_found", query: query)
66+
redirect_to badge_batches_path
67+
end
68+
end
69+
70+
private
71+
72+
def set_badge_batch
73+
@badge_batch = BadgeBatch.find(params[:id])
74+
end
75+
76+
def badge_batch_params
77+
params.require(:badge_batch).permit(:count, :note)
78+
end
79+
80+
def note_param
81+
params.require(:badge_batch).permit(:note)
82+
end
83+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
class BadgesController < ApplicationController
5+
before_action :require_admin
6+
before_action :set_badge, only: %i[show edit update]
7+
8+
def show
9+
@badge_batch = @badge.badge_batch
10+
@units = Unit.where(id: @badge.id)
11+
end
12+
13+
def edit
14+
end
15+
16+
def update
17+
if @badge.update(note_params)
18+
flash[:success] = t("badges.messages.badge_updated")
19+
redirect_to badge_batch_path(@badge.badge_batch)
20+
else
21+
render :edit
22+
end
23+
end
24+
25+
private
26+
27+
def set_badge
28+
@badge = Badge.find(params[:id])
29+
end
30+
31+
def note_params
32+
params.require(:badge).permit(:note)
33+
end
34+
end

app/javascript/dirty_forms.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ class DirtyForms {
5858
formAction.includes("/login") ||
5959
formAction.includes("/register") ||
6060
formAction.includes("/safety_standards") ||
61-
formAction.includes("/search")
61+
formAction.includes("/search") ||
62+
formAction.includes("/badge_batches")
6263
) {
6364
return;
6465
}

app/models/badge.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
class Badge < ApplicationRecord
5+
extend T::Sig
6+
7+
include CustomIdGenerator
8+
9+
belongs_to :badge_batch
10+
end

app/models/badge_batch.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
class BadgeBatch < ApplicationRecord
5+
extend T::Sig
6+
7+
has_many :badges, dependent: :destroy
8+
end

app/models/concerns/custom_id_generator.rb

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,50 @@ module CustomIdGenerator
1919
class_methods do
2020
extend T::Sig
2121

22-
sig do
23-
params(scope_conditions: T::Hash[T.untyped, T.untyped]).returns(String)
24-
end
25-
def generate_random_id(scope_conditions = {})
22+
sig { returns(String) }
23+
def generate_random_id
2624
loop do
2725
raw_id = SecureRandom.alphanumeric(32).upcase
2826
filtered_chars = raw_id.chars.reject do |char|
2927
AMBIGUOUS_CHARS.include?(char)
3028
end
3129
id = filtered_chars.first(ID_LENGTH).join
3230
next if id.length < ID_LENGTH
33-
break id unless exists?({id: id}.merge(scope_conditions))
31+
break id unless exists?(id: id)
32+
end
33+
end
34+
35+
sig { params(count: Integer).returns(T::Array[String]) }
36+
def generate_random_ids(count)
37+
return [] if count <= 0
38+
39+
needed = count
40+
generated_ids = []
41+
42+
while needed > 0
43+
# Generate a batch of candidate IDs
44+
candidates = needed.times.map { generate_single_id_string }
45+
46+
# Check which ones already exist (single DB query)
47+
existing = where(id: candidates).pluck(:id)
48+
new_ids = candidates - existing
49+
50+
generated_ids.concat(new_ids)
51+
needed -= new_ids.length
52+
end
53+
54+
generated_ids.first(count)
55+
end
56+
57+
sig { returns(String) }
58+
def generate_single_id_string
59+
loop do
60+
raw_id = SecureRandom.alphanumeric(32).upcase
61+
filtered_chars = raw_id.chars.reject do |char|
62+
AMBIGUOUS_CHARS.include?(char)
63+
end
64+
id = filtered_chars.first(ID_LENGTH).join
65+
return id if id.length == ID_LENGTH
3466
end
3567
end
3668
end
@@ -39,7 +71,6 @@ def generate_random_id(scope_conditions = {})
3971

4072
sig { void }
4173
def generate_custom_id
42-
scope_conditions = respond_to?(:uniqueness_scope) ? uniqueness_scope : {}
43-
self.id = self.class.generate_random_id(scope_conditions)
74+
self.id = self.class.generate_random_id
4475
end
4576
end

app/views/admin/index.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<li><%= link_to t("navigation.users"), users_path %></li>
88
<li><%= link_to t("inspector_companies.titles.index"), inspector_companies_path %></li>
99
<li><%= link_to t("navigation.pages"), pages_path %></li>
10+
<li><%= link_to t("navigation.badges"), badge_batches_path %></li>
1011
<li><%= link_to t("navigation.jobs"), "/mission_control" %></li>
1112
<li><%= link_to t("navigation.releases"), admin_releases_path %></li>
1213
<li><%= link_to t("navigation.files"), admin_files_path %></li>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<%= render 'chobble_forms/form_context',
2+
model: @badge_batch,
3+
i18n_base: 'forms.badge_batch_edit',
4+
url: badge_batch_path(@badge_batch) do |form| %>
5+
<%= render 'chobble_forms/fieldset', legend_key: 'batch_details' do %>
6+
<%= render 'chobble_forms/text_area', field: :note %>
7+
<% end %>
8+
<% end %>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<%= render 'shared/page_header', title: t("badges.titles.index") %>
2+
3+
<nav>
4+
<%= link_to t("badges.buttons.new_batch"), new_badge_batch_path %>
5+
</nav>
6+
7+
<%= form_with url: search_badge_batches_path, method: :get, data: { turbo: false }, id: "search-box" do |form| %>
8+
<%= form.text_field :query,
9+
placeholder: t('badges.search.placeholder'),
10+
value: params[:query],
11+
onkeyup: "if(event.key === 'Enter') this.form.submit();" %>
12+
<% end %>
13+
14+
<% if @badge_batches.any? %>
15+
<div class="table-list badge-batches-list">
16+
<div class="table-list-header">
17+
<%= render 'shared/table_column',
18+
field: :id,
19+
i18n_key: "badges.fields.id",
20+
header: true %>
21+
<%= render 'shared/table_column',
22+
field: :count,
23+
i18n_key: "badges.fields.count",
24+
header: true %>
25+
<%= render 'shared/table_column',
26+
field: :created_at,
27+
i18n_key: "badges.fields.created_at",
28+
header: true %>
29+
<%= render 'shared/table_column',
30+
field: :note,
31+
i18n_key: "badges.fields.note",
32+
header: true %>
33+
</div>
34+
35+
<ul class="table-list-items">
36+
<% @badge_batches.each do |batch| %>
37+
<li>
38+
<%= link_to badge_batch_path(batch), class: "table-list-link" do %>
39+
<%= render 'shared/table_column',
40+
field: :id,
41+
i18n_key: "badges.fields.id",
42+
item: batch %>
43+
<%= render 'shared/table_column',
44+
field: :count,
45+
i18n_key: "badges.fields.count",
46+
item: batch %>
47+
<%= render 'shared/table_column',
48+
field: :created_at,
49+
i18n_key: "badges.fields.created_at" do %>
50+
<%= l(batch.created_at, format: :long) %>
51+
<% end %>
52+
<%= render 'shared/table_column',
53+
field: :note,
54+
i18n_key: "badges.fields.note",
55+
item: batch %>
56+
<% end %>
57+
</li>
58+
<% end %>
59+
</ul>
60+
</div>
61+
<% else %>
62+
<p><%= t("badges.messages.no_batches") %></p>
63+
<% end %>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<%= render 'chobble_forms/form_context',
2+
model: @badge_batch,
3+
i18n_base: 'forms.badge_batch',
4+
url: badge_batches_path do |form| %>
5+
<%= render 'chobble_forms/fieldset', legend_key: 'batch_details' do %>
6+
<%= render 'chobble_forms/number', field: :count, required: true %>
7+
<%= render 'chobble_forms/text_area', field: :note %>
8+
<% end %>
9+
<% end %>

0 commit comments

Comments
 (0)