From 98ad5b3171a0c578f9232f5333b8d6c0d6cf5c18 Mon Sep 17 00:00:00 2001 From: Andrew Ohnstad Date: Wed, 17 Dec 2025 20:03:55 -0500 Subject: [PATCH 1/2] Remove old Channel-to-Zone direct relationships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add migration to remove codeplug_id column from zones table - Remove belongs_to :codeplug from Zone model - Update Codeplug model to access zones through codeplug_zones - Remove nested zone routes under codeplugs - Clean up ZonesController to only handle standalone zones - Update zone views to use standalone paths only - Remove ChannelZonesController (no longer needed) - Update tests and factories to reflect new architecture - Add comprehensive seed data demonstrating new zone architecture This completes the zone architecture refactor. Zones are now standalone entities owned by users, linked to codeplugs via the CodeplugZone join table. The old direct codeplug_id foreign key relationship has been removed. Closes #105 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/controllers/channel_zones_controller.rb | 59 --- app/controllers/zones_controller.rb | 139 ++------ app/models/codeplug.rb | 2 +- app/models/zone.rb | 1 - app/views/codeplugs/show.html.erb | 33 +- app/views/zones/_form.html.erb | 22 +- app/views/zones/edit.html.erb | 6 +- app/views/zones/index.html.erb | 83 ++--- app/views/zones/new.html.erb | 6 +- app/views/zones/show.html.erb | 279 ++++++--------- config/routes.rb | 7 - ...218005215_remove_codeplug_id_from_zones.rb | 12 + db/schema.rb | 5 +- db/seeds.rb | 145 ++++++++ .../channel_zones_controller_test.rb | 143 -------- test/controllers/zones_controller_test.rb | 336 +++++------------- test/factories/codeplug_zones.rb | 5 - test/factories/zones.rb | 1 - test/models/codeplug_test.rb | 27 +- test/models/codeplug_zone_test.rb | 67 ++-- test/models/zone_test.rb | 112 +++--- test/system/zones_test.rb | 258 ++------------ 22 files changed, 564 insertions(+), 1184 deletions(-) delete mode 100644 app/controllers/channel_zones_controller.rb create mode 100644 db/migrate/20251218005215_remove_codeplug_id_from_zones.rb delete mode 100644 test/controllers/channel_zones_controller_test.rb diff --git a/app/controllers/channel_zones_controller.rb b/app/controllers/channel_zones_controller.rb deleted file mode 100644 index 0f4ee8a..0000000 --- a/app/controllers/channel_zones_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -class ChannelZonesController < ApplicationController - before_action :set_codeplug - before_action :set_zone - before_action :authorize_codeplug - before_action :set_channel_zone, only: [ :destroy ] - - def create - @channel = @codeplug.channels.find_by(id: channel_zone_params[:channel_id]) - - unless @channel - flash[:alert] = "Channel not found in this codeplug." - redirect_to codeplug_zone_path(@codeplug, @zone), status: :unprocessable_entity - return - end - - # Get the next available position - max_position = @zone.channel_zones.maximum(:position) || 0 - @channel_zone = @zone.channel_zones.new( - channel: @channel, - position: max_position + 1 - ) - - if @channel_zone.save - redirect_to codeplug_zone_path(@codeplug, @zone), notice: "Channel was successfully added to zone." - else - flash[:alert] = @channel_zone.errors.full_messages.join(", ") - redirect_to codeplug_zone_path(@codeplug, @zone), status: :unprocessable_entity - end - end - - def destroy - @channel_zone.destroy! - redirect_to codeplug_zone_path(@codeplug, @zone), notice: "Channel was successfully removed from zone." - end - - private - - def set_codeplug - @codeplug = Codeplug.find(params[:codeplug_id]) - end - - def set_zone - @zone = @codeplug.zones.find(params[:zone_id]) - end - - def set_channel_zone - @channel_zone = @zone.channel_zones.find(params[:id]) - end - - def authorize_codeplug - unless @codeplug.user == current_user - redirect_to codeplugs_path, alert: "You don't have permission to access this codeplug." - end - end - - def channel_zone_params - params.require(:channel_zone).permit(:channel_id) - end -end diff --git a/app/controllers/zones_controller.rb b/app/controllers/zones_controller.rb index 11ec490..f8ef8be 100644 --- a/app/controllers/zones_controller.rb +++ b/app/controllers/zones_controller.rb @@ -1,75 +1,37 @@ class ZonesController < ApplicationController - # Only run these for nested routes (when codeplug_id is present) - before_action :set_codeplug, if: :nested_route? - before_action :authorize_codeplug, if: :nested_route? before_action :set_zone, only: [ :show, :edit, :update, :destroy, :update_positions ] - before_action :authorize_zone, only: [ :show, :edit, :update, :destroy, :update_positions ], unless: :nested_route? + before_action :authorize_zone_view, only: [ :show ] + before_action :authorize_zone_edit, only: [ :edit, :update, :destroy, :update_positions ] def index - if nested_route? - # Nested route: show zones for specific codeplug - @zones = @codeplug.zones.order(:name) - else - # Standalone route: show zones available to current user - @zones = Zone.available_to_user(current_user).order(:name) - end + @zones = Zone.available_to_user(current_user).order(:name) end def show - if nested_route? - # Nested route: Get channels for codeplug - channel_ids_in_zone = @zone.channel_zones.pluck(:channel_id) - - @available_channels = @codeplug.channels - .where.not(id: channel_ids_in_zone) - .order(:long_name) - else - # Standalone route: Show zone systems (placeholder for future functionality) - @zone_systems = @zone.zone_systems.includes(:system).order(:position) - end + @zone_systems = @zone.zone_systems.includes(:system).order(:position) end def new - if nested_route? - @zone = @codeplug.zones.new - else - @zone = Zone.new - end + @zone = Zone.new end def edit - # Edit zone form end def create - if nested_route? - @zone = @codeplug.zones.new(zone_params) - @zone.user = current_user - - if @zone.save - redirect_to codeplug_zone_path(@codeplug, @zone), notice: "Zone was successfully created." - else - render :new, status: :unprocessable_entity - end - else - @zone = Zone.new(zone_params) - @zone.user = current_user + @zone = Zone.new(zone_params) + @zone.user = current_user - if @zone.save - redirect_to zone_path(@zone), notice: "Zone was successfully created." - else - render :new, status: :unprocessable_entity - end + if @zone.save + redirect_to zone_path(@zone), notice: "Zone was successfully created." + else + render :new, status: :unprocessable_entity end end def update if @zone.update(zone_params) - if nested_route? - redirect_to codeplug_zone_path(@codeplug, @zone), notice: "Zone was successfully updated." - else - redirect_to zone_path(@zone), notice: "Zone was successfully updated." - end + redirect_to zone_path(@zone), notice: "Zone was successfully updated." else render :edit, status: :unprocessable_entity end @@ -77,43 +39,23 @@ def update def destroy @zone.destroy! - if nested_route? - redirect_to codeplug_zones_path(@codeplug), notice: "Zone was successfully deleted." - else - redirect_to zones_path, notice: "Zone was successfully deleted." - end + redirect_to zones_path, notice: "Zone was successfully deleted." end def update_positions positions_params = params.permit(positions: [ :id, :position ]) ActiveRecord::Base.transaction do - if nested_route? - # Update channel positions for codeplug zones - # First pass: Set temporary positions to avoid uniqueness conflicts - positions_params[:positions].each_with_index do |position_data, index| - channel_zone = @zone.channel_zones.find(position_data[:id]) - channel_zone.update_column(:position, 1000 + index) - end - - # Second pass: Set actual positions - positions_params[:positions].each do |position_data| - channel_zone = @zone.channel_zones.find(position_data[:id]) - channel_zone.update!(position: position_data[:position]) - end - else - # Update zone_system positions for standalone zones - # First pass: Set temporary positions to avoid uniqueness conflicts - positions_params[:positions].each_with_index do |position_data, index| - zone_system = @zone.zone_systems.find(position_data[:id]) - zone_system.update_column(:position, 1000 + index) - end - - # Second pass: Set actual positions - positions_params[:positions].each do |position_data| - zone_system = @zone.zone_systems.find(position_data[:id]) - zone_system.update!(position: position_data[:position]) - end + # First pass: Set temporary positions to avoid uniqueness conflicts + positions_params[:positions].each_with_index do |position_data, index| + zone_system = @zone.zone_systems.find(position_data[:id]) + zone_system.update_column(:position, 1000 + index) + end + + # Second pass: Set actual positions + positions_params[:positions].each do |position_data| + zone_system = @zone.zone_systems.find(position_data[:id]) + zone_system.update!(position: position_data[:position]) end end @@ -122,43 +64,22 @@ def update_positions private - def set_codeplug - @codeplug = Codeplug.find(params[:codeplug_id]) - end - def set_zone - if nested_route? - @zone = @codeplug.zones.find(params[:id]) - else - @zone = Zone.find(params[:id]) - end + @zone = Zone.find(params[:id]) end - def authorize_codeplug - unless @codeplug.user == current_user - redirect_to codeplugs_path, alert: "You don't have permission to access this codeplug." + def authorize_zone_view + unless @zone.viewable_by?(current_user) + head :forbidden end end - def authorize_zone - # For standalone routes, check if user can view/edit the zone - action = action_name.to_sym - - if [ :show ].include?(action) - unless @zone.viewable_by?(current_user) - head :forbidden - end - elsif [ :edit, :update, :destroy, :update_positions ].include?(action) - unless @zone.editable_by?(current_user) - head :forbidden - end + def authorize_zone_edit + unless @zone.editable_by?(current_user) + head :forbidden end end - def nested_route? - params[:codeplug_id].present? - end - def zone_params params.require(:zone).permit(:name, :long_name, :short_name, :public) end diff --git a/app/models/codeplug.rb b/app/models/codeplug.rb index cbe729f..156d6dd 100644 --- a/app/models/codeplug.rb +++ b/app/models/codeplug.rb @@ -1,9 +1,9 @@ class Codeplug < ApplicationRecord # Associations belongs_to :user - has_many :zones, dependent: :destroy has_many :channels, dependent: :destroy has_many :codeplug_zones, dependent: :destroy + has_many :zones, through: :codeplug_zones # Validations validates :name, presence: true diff --git a/app/models/zone.rb b/app/models/zone.rb index 84235ca..f0fc189 100644 --- a/app/models/zone.rb +++ b/app/models/zone.rb @@ -1,7 +1,6 @@ class Zone < ApplicationRecord # Associations belongs_to :user - belongs_to :codeplug, optional: true has_many :channel_zones, dependent: :destroy has_many :channels, through: :channel_zones has_many :codeplug_zones, dependent: :destroy diff --git a/app/views/codeplugs/show.html.erb b/app/views/codeplugs/show.html.erb index 9cdbae5..f135423 100644 --- a/app/views/codeplugs/show.html.erb +++ b/app/views/codeplugs/show.html.erb @@ -146,38 +146,7 @@ - <%# Legacy Zones Section (codeplug-owned zones) %> -
-
-
-
-
Zones
- <%= link_to "Manage Zones", codeplug_zones_path(@codeplug), class: "btn btn-sm btn-primary" %> -
- - <% if @codeplug.zones.any? %> -

<%= pluralize(@codeplug.zones.count, "zone") %>

-
    - <% @codeplug.zones.limit(5).each do |zone| %> -
  • - <%= zone.long_name || zone.name %> - <%= zone.channels.count %> channels -
  • - <% end %> - <% if @codeplug.zones.count > 5 %> -
  • - + <%= @codeplug.zones.count - 5 %> more zones -
  • - <% end %> -
- <% else %> -

No zones configured yet.

- <% end %> -
-
-
- -
+
diff --git a/app/views/zones/_form.html.erb b/app/views/zones/_form.html.erb index b9a07d2..c127cd0 100644 --- a/app/views/zones/_form.html.erb +++ b/app/views/zones/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_with model: @codeplug ? [ @codeplug, zone ] : zone do |f| %> +<%= form_with model: zone do |f| %> <% if zone.errors.any? %>
<%= pluralize(zone.errors.count, "error") %> prohibited this zone from being saved:
@@ -28,22 +28,16 @@
Abbreviated name (for radios with limited display)
- <% unless @codeplug %> -
-
- <%= f.check_box :public, class: "form-check-input" %> - <%= f.label :public, "Make this zone public", class: "form-check-label" %> -
Public zones can be viewed by all users. Private zones are only visible to you.
-
+
+
+ <%= f.check_box :public, class: "form-check-input" %> + <%= f.label :public, "Make this zone public", class: "form-check-label" %> +
Public zones can be viewed by all users. Private zones are only visible to you.
- <% end %> +
<%= f.submit class: "btn btn-primary" %> - <% if @codeplug %> - <%= link_to "Cancel", zone.persisted? ? codeplug_zone_path(@codeplug, zone) : codeplug_zones_path(@codeplug), class: "btn btn-secondary" %> - <% else %> - <%= link_to "Cancel", zone.persisted? ? zone_path(zone) : zones_path, class: "btn btn-secondary" %> - <% end %> + <%= link_to "Cancel", zone.persisted? ? zone_path(zone) : zones_path, class: "btn btn-secondary" %>
<% end %> diff --git a/app/views/zones/edit.html.erb b/app/views/zones/edit.html.erb index 7ea2708..8c47c93 100644 --- a/app/views/zones/edit.html.erb +++ b/app/views/zones/edit.html.erb @@ -1,11 +1,7 @@

Edit Zone - <%= @zone.name %>

- <% if @codeplug %> - <%= link_to "← Back to Zone", codeplug_zone_path(@codeplug, @zone), class: "btn btn-secondary mb-3" %> - <% else %> - <%= link_to "← Back to Zone", zone_path(@zone), class: "btn btn-secondary mb-3" %> - <% end %> + <%= link_to "← Back to Zone", zone_path(@zone), class: "btn btn-secondary mb-3" %>
diff --git a/app/views/zones/index.html.erb b/app/views/zones/index.html.erb index 4fecf99..f95fa9d 100644 --- a/app/views/zones/index.html.erb +++ b/app/views/zones/index.html.erb @@ -1,17 +1,9 @@

Zones

- <% if @codeplug %> - <%= link_to "New Zone", new_codeplug_zone_path(@codeplug), class: "btn btn-primary" %> - <% else %> - <%= link_to "New Zone", new_zone_path, class: "btn btn-primary" %> - <% end %> + <%= link_to "New Zone", new_zone_path, class: "btn btn-primary" %>
- <% if @codeplug %> - <%= link_to "← Back to Codeplug", codeplug_path(@codeplug), class: "btn btn-secondary mb-3" %> - <% end %> - <% if @zones.any? %>
@@ -20,62 +12,41 @@ - <% unless @codeplug %> - - - - <% else %> - - <% end %> + + + <% @zones.each do |zone| %> - <% if @codeplug %> - - <% else %> - - <% end %> + - <% if @codeplug %> - - <% else %> - - - - <% end %> + + + @@ -85,11 +56,7 @@ <% else %>
- <% if @codeplug %> - No zones found. <%= link_to "Create the first one", new_codeplug_zone_path(@codeplug), class: "alert-link" %>. - <% else %> - No zones found. <%= link_to "Create the first one", new_zone_path, class: "alert-link" %>. - <% end %> + No zones found. <%= link_to "Create the first one", new_zone_path, class: "alert-link" %>.
<% end %> diff --git a/app/views/zones/new.html.erb b/app/views/zones/new.html.erb index 59fae00..d232f3b 100644 --- a/app/views/zones/new.html.erb +++ b/app/views/zones/new.html.erb @@ -1,11 +1,7 @@

New Zone

- <% if @codeplug %> - <%= link_to "← Back to Zones", codeplug_zones_path(@codeplug), class: "btn btn-secondary mb-3" %> - <% else %> - <%= link_to "← Back to Zones", zones_path, class: "btn btn-secondary mb-3" %> - <% end %> + <%= link_to "← Back to Zones", zones_path, class: "btn btn-secondary mb-3" %>
diff --git a/app/views/zones/show.html.erb b/app/views/zones/show.html.erb index c87f386..1967cae 100644 --- a/app/views/zones/show.html.erb +++ b/app/views/zones/show.html.erb @@ -2,13 +2,7 @@

<%= @zone.name %>

- <% if @codeplug %> - <%= link_to "Edit", edit_codeplug_zone_path(@codeplug, @zone), class: "btn btn-secondary me-2" %> - <%= button_to "Delete", codeplug_zone_path(@codeplug, @zone), method: :delete, - class: "btn btn-danger", - form: { class: "d-inline" }, - data: { turbo_confirm: "Are you sure?" } %> - <% elsif @zone.editable_by?(current_user) %> + <% if @zone.editable_by?(current_user) %> <%= link_to "Edit", edit_zone_path(@zone), class: "btn btn-secondary me-2" %> <%= button_to "Delete", zone_path(@zone), method: :delete, class: "btn btn-danger", @@ -18,11 +12,7 @@
- <% if @codeplug %> - <%= link_to "← Back to Zones", codeplug_zones_path(@codeplug), class: "btn btn-secondary mb-3" %> - <% else %> - <%= link_to "← Back to Zones", zones_path, class: "btn btn-secondary mb-3" %> - <% end %> + <%= link_to "← Back to Zones", zones_path, class: "btn btn-secondary mb-3" %>
@@ -40,202 +30,135 @@
- <% unless @codeplug %> -
-
- Owner: <%= @zone.user.email %> -
-
- Visibility: - <% if @zone.public? %> - Public - <% else %> - Private - <% end %> -
+
+
+ Owner: <%= @zone.user.email %>
- <% end %> +
+ Visibility: + <% if @zone.public? %> + Public + <% else %> + Private + <% end %> +
+
- <% if @codeplug %> -
-
-
-
Channels
-
+
+
+
+
Systems
+
- <% if @available_channels.any? %> + <% if @zone.editable_by?(current_user) %> + <% available_systems = System.where.not(id: @zone.zone_systems.pluck(:system_id)).order(:name) %> + <% if available_systems.any? %>
- <%= form_with(model: @zone.channel_zones.new, url: codeplug_zone_channel_zones_path(@codeplug, @zone), method: :post, local: true, class: "row g-3") do |f| %> + <%= form_with(model: @zone.zone_systems.new, url: zone_zone_systems_path(@zone), method: :post, local: true, class: "row g-3") do |f| %>
- <%= f.select :channel_id, - options_from_collection_for_select(@available_channels, :id, :long_name), - { prompt: "Select a channel to add..." }, + <%= f.select :system_id, + options_from_collection_for_select(available_systems, :id, :name), + { prompt: "Select a system to add..." }, { class: "form-select" } %>
- <%= f.submit "Add Channel", class: "btn btn-primary" %> + <%= f.submit "Add System", class: "btn btn-primary" %>
<% end %>
<% end %> + <% end %> - <% if @zone.channel_zones.any? %> -

<%= pluralize(@zone.channel_zones.count, "channel") %>

-
- <% @zone.channel_zones.order(:position).each do |channel_zone| %> -
-
-
+ <% if @zone_systems.any? %> +

<%= pluralize(@zone_systems.count, "system") %>

+
+ <% @zone_systems.each do |zone_system| %> +
+
+
+ <% if @zone.editable_by?(current_user) %> -
- <%= channel_zone.channel.long_name %> - <% if channel_zone.channel.system %> - <%= channel_zone.channel.system.mode.upcase %> + <% end %> +
+ <%= zone_system.system.name %> + <%= zone_system.system.mode.upcase %> +
+ + <%= zone_system.system.rx_frequency %> MHz + <% if zone_system.system.tx_frequency != zone_system.system.rx_frequency %> + / <%= zone_system.system.tx_frequency %> MHz <% end %> -
- <%= channel_zone.position %> - <%= link_to "Edit", edit_codeplug_channel_path(@codeplug, channel_zone.channel), class: "btn btn-sm btn-outline-secondary me-2" %> - <%= button_to "Remove", codeplug_zone_channel_zone_path(@codeplug, @zone, channel_zone), - method: :delete, - class: "btn btn-sm btn-outline-danger", - form: { class: "d-inline" }, - data: { turbo_confirm: "Remove this channel from the zone?" } %> -
-
-
- <% end %> -
- <% else %> -

No channels in this zone yet. <%= "Add channels using the form above." if @available_channels.any? %>

- <% end %> -
-
- <% else %> - <%# Standalone zone view - show systems %> -
-
-
-
Systems
-
- - <% if @zone.editable_by?(current_user) %> - <% available_systems = System.where.not(id: @zone.zone_systems.pluck(:system_id)).order(:name) %> - <% if available_systems.any? %> -
-
- <%= form_with(model: @zone.zone_systems.new, url: zone_zone_systems_path(@zone), method: :post, local: true, class: "row g-3") do |f| %> -
- <%= f.select :system_id, - options_from_collection_for_select(available_systems, :id, :name), - { prompt: "Select a system to add..." }, - { class: "form-select" } %> -
-
- <%= f.submit "Add System", class: "btn btn-primary" %> -
- <% end %> -
-
- <% end %> - <% end %> - - <% if @zone_systems.any? %> -

<%= pluralize(@zone_systems.count, "system") %>

-
- <% @zone_systems.each do |zone_system| %> -
-
-
- <% if @zone.editable_by?(current_user) %> - - - - - - <% end %> -
- <%= zone_system.system.name %> - <%= zone_system.system.mode.upcase %> -
- - <%= zone_system.system.rx_frequency %> MHz - <% if zone_system.system.tx_frequency != zone_system.system.rx_frequency %> - / <%= zone_system.system.tx_frequency %> MHz - <% end %> - - <% if zone_system.system.mode == "dmr" || zone_system.system.mode == "p25" %> -
- Talkgroups: - <% if zone_system.zone_system_talkgroups.any? %> -
- <% zone_system.zone_system_talkgroups.includes(system_talk_group: :talk_group).each do |zstg| %> - - <%= zstg.system_talk_group.talk_group.name %> - (<%= zstg.system_talk_group.talk_group.talkgroup_number %>) - TS<%= zstg.system_talk_group.timeslot %> - <% if @zone.editable_by?(current_user) %> - <%= button_to zone_zone_system_zone_system_talkgroup_path(@zone, zone_system, zstg), - method: :delete, - class: "btn btn-sm p-0 ms-1 text-danger", - form: { class: "d-inline" }, - data: { turbo_confirm: "Remove this talkgroup?" } do %> - - - - <% end %> + + <% if zone_system.system.mode == "dmr" || zone_system.system.mode == "p25" %> +
+ Talkgroups: + <% if zone_system.zone_system_talkgroups.any? %> +
+ <% zone_system.zone_system_talkgroups.includes(system_talk_group: :talk_group).each do |zstg| %> + + <%= zstg.system_talk_group.talk_group.name %> + (<%= zstg.system_talk_group.talk_group.talkgroup_number %>) + TS<%= zstg.system_talk_group.timeslot %> + <% if @zone.editable_by?(current_user) %> + <%= button_to zone_zone_system_zone_system_talkgroup_path(@zone, zone_system, zstg), + method: :delete, + class: "btn btn-sm p-0 ms-1 text-danger", + form: { class: "d-inline" }, + data: { turbo_confirm: "Remove this talkgroup?" } do %> + + + <% end %> - - <% end %> -
- <% else %> - None selected - <% end %> - <% if @zone.editable_by?(current_user) %> - <% available_talkgroups = zone_system.system.system_talk_groups.includes(:talk_group).where.not(id: zone_system.zone_system_talkgroups.pluck(:system_talk_group_id)) %> - <% if available_talkgroups.any? %> -
- <%= form_with(model: zone_system.zone_system_talkgroups.new, url: zone_zone_system_zone_system_talkgroups_path(@zone, zone_system), method: :post, local: true, class: "d-flex gap-2 align-items-center") do |f| %> - <%= f.select :system_talk_group_id, - available_talkgroups.map { |stg| ["#{stg.talk_group.name} (#{stg.talk_group.talkgroup_number}) - TS#{stg.timeslot}", stg.id] }, - { prompt: "Select talkgroup..." }, - { class: "form-select form-select-sm", style: "width: auto;" } %> - <%= f.submit "Add Talkgroup", class: "btn btn-sm btn-outline-primary" %> <% end %> -
+ <% end %> +
+ <% else %> + None selected + <% end %> + <% if @zone.editable_by?(current_user) %> + <% available_talkgroups = zone_system.system.system_talk_groups.includes(:talk_group).where.not(id: zone_system.zone_system_talkgroups.pluck(:system_talk_group_id)) %> + <% if available_talkgroups.any? %> +
+ <%= form_with(model: zone_system.zone_system_talkgroups.new, url: zone_zone_system_zone_system_talkgroups_path(@zone, zone_system), method: :post, local: true, class: "d-flex gap-2 align-items-center") do |f| %> + <%= f.select :system_talk_group_id, + available_talkgroups.map { |stg| ["#{stg.talk_group.name} (#{stg.talk_group.talkgroup_number}) - TS#{stg.timeslot}", stg.id] }, + { prompt: "Select talkgroup..." }, + { class: "form-select form-select-sm", style: "width: auto;" } %> + <%= f.submit "Add Talkgroup", class: "btn btn-sm btn-outline-primary" %> + <% end %> +
<% end %> -
- <% end %> -
- <%= zone_system.position %> - <% if @zone.editable_by?(current_user) %> - <%= button_to "Remove", zone_zone_system_path(@zone, zone_system), - method: :delete, - class: "btn btn-sm btn-outline-danger", - form: { class: "d-inline" }, - data: { turbo_confirm: "Remove this system from the zone?" } %> + <% end %> +
<% end %>
+ <%= zone_system.position %> + <% if @zone.editable_by?(current_user) %> + <%= button_to "Remove", zone_zone_system_path(@zone, zone_system), + method: :delete, + class: "btn btn-sm btn-outline-danger", + form: { class: "d-inline" }, + data: { turbo_confirm: "Remove this system from the zone?" } %> + <% end %>
- <% end %> -
- <% else %> -

No systems in this zone yet. <%= "Add systems using the form above." if @zone.editable_by?(current_user) && System.count > 0 %>

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

No systems in this zone yet. <%= "Add systems using the form above." if @zone.editable_by?(current_user) && System.count > 0 %>

+ <% end %>
- <% end %> +
diff --git a/config/routes.rb b/config/routes.rb index 683c5f0..a63f633 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,13 +52,6 @@ patch :update_positions end end - # Nested zones routes (kept for backward compatibility, will be deprecated) - resources :zones do - resources :channel_zones, only: [ :create, :destroy ] - member do - patch :update_positions - end - end resources :channels end diff --git a/db/migrate/20251218005215_remove_codeplug_id_from_zones.rb b/db/migrate/20251218005215_remove_codeplug_id_from_zones.rb new file mode 100644 index 0000000..a699846 --- /dev/null +++ b/db/migrate/20251218005215_remove_codeplug_id_from_zones.rb @@ -0,0 +1,12 @@ +class RemoveCodeplugIdFromZones < ActiveRecord::Migration[8.1] + def change + # Remove the foreign key constraint first + remove_foreign_key :zones, :codeplugs + + # Remove the index + remove_index :zones, :codeplug_id + + # Remove the column + remove_column :zones, :codeplug_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index da3c5c0..7bcba1a 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: 2025_12_15_054858) do +ActiveRecord::Schema[8.1].define(version: 2025_12_18_005215) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -227,7 +227,6 @@ end create_table "zones", force: :cascade do |t| - t.bigint "codeplug_id" t.datetime "created_at", null: false t.string "long_name" t.string "name", null: false @@ -235,7 +234,6 @@ t.string "short_name" t.datetime "updated_at", null: false t.bigint "user_id", null: false - t.index ["codeplug_id"], name: "index_zones_on_codeplug_id" t.index ["public"], name: "index_zones_on_public" t.index ["user_id"], name: "index_zones_on_user_id" end @@ -263,6 +261,5 @@ add_foreign_key "zone_system_talk_groups", "zone_systems" add_foreign_key "zone_systems", "systems" add_foreign_key "zone_systems", "zones" - add_foreign_key "zones", "codeplugs" add_foreign_key "zones", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index cc0d224..24165d1 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -222,6 +222,151 @@ end puts "Created #{radio_models_data.count} system radio models" + # Create networks for DMR talkgroups + networks_data = [ + { name: "Brandmeister", network_type: "Digital-DMR", description: "Worldwide DMR network" }, + { name: "TGIF", network_type: "Digital-DMR", description: "The Global Internet of Friends" }, + { name: "P25 Network", network_type: "Digital-P25", description: "Project 25 digital network" } + ] + + networks = networks_data.map do |data| + network = Network.find_or_create_by!(name: data[:name]) do |n| + n.network_type = data[:network_type] + n.description = data[:description] + end + network + end + puts "Created #{networks.count} networks" + + # Create some sample systems (repeaters) + dmr_network = Network.find_by!(name: "Brandmeister") + + # Analog system + analog_detail = AnalogModeDetail.find_or_create_by!(id: AnalogModeDetail.maximum(:id).to_i + 1) + analog_system = System.find_or_create_by!(name: "W1DEV Analog Repeater") do |s| + s.mode = "analog" + s.rx_frequency = 146.82 + s.tx_frequency = 146.22 + s.supports_tx_tone = true + s.tx_tone_value = "127.3" + s.supports_rx_tone = true + s.rx_tone_value = "127.3" + s.mode_detail = analog_detail + end + puts "Created analog system: #{analog_system.name}" + + # DMR system + dmr_detail = DmrModeDetail.find_or_create_by!(color_code: 3) do |d| + d.id = DmrModeDetail.maximum(:id).to_i + 1 + end + dmr_system = System.find_or_create_by!(name: "W1DEV DMR Repeater") do |s| + s.mode = "dmr" + s.rx_frequency = 446.5 + s.tx_frequency = 441.5 + s.mode_detail = dmr_detail + end + # Associate DMR system with network + unless dmr_system.networks.include?(dmr_network) + dmr_system.networks << dmr_network + end + puts "Created DMR system: #{dmr_system.name}" + + # Create talkgroups + talkgroups_data = [ + { name: "Local", talkgroup_number: "3100", network: dmr_network }, + { name: "TAC 310", talkgroup_number: "310", network: dmr_network }, + { name: "North America", talkgroup_number: "3", network: dmr_network } + ] + + talkgroups = talkgroups_data.map do |data| + TalkGroup.find_or_create_by!(network: data[:network], talkgroup_number: data[:talkgroup_number]) do |tg| + tg.name = data[:name] + end + end + puts "Created #{talkgroups.count} talkgroups" + + # Create SystemTalkGroups (talkgroups available on the DMR system) + stg_data = [ + { talk_group: TalkGroup.find_by!(name: "Local"), timeslot: 1 }, + { talk_group: TalkGroup.find_by!(name: "TAC 310"), timeslot: 2 }, + { talk_group: TalkGroup.find_by!(name: "North America"), timeslot: 1 } + ] + + stg_data.each do |data| + SystemTalkGroup.find_or_create_by!( + system: dmr_system, + talk_group: data[:talk_group], + timeslot: data[:timeslot] + ) + end + puts "Created #{stg_data.count} system talkgroups" + + # Create standalone zones (new architecture) + # Zone 1: Local analog repeaters + analog_zone = Zone.find_or_create_by!(user: user, name: "Local Analog") do |z| + z.long_name = "Local Analog Repeaters" + z.short_name = "ANALG" + z.public = false + end + + # Add analog system to zone + unless analog_zone.zone_systems.exists?(system: analog_system) + ZoneSystem.create!(zone: analog_zone, system: analog_system, position: 1) + end + puts "Created zone: #{analog_zone.name} with #{analog_zone.zone_systems.count} system(s)" + + # Zone 2: DMR with talkgroups + dmr_zone = Zone.find_or_create_by!(user: user, name: "DMR Repeaters") do |z| + z.long_name = "DMR Repeater Zone" + z.short_name = "DMR" + z.public = false + end + + # Add DMR system to zone + zone_system = dmr_zone.zone_systems.find_or_create_by!(system: dmr_system) do |zs| + zs.position = 1 + end + + # Add talkgroups to zone_system + stg_data.each do |data| + stg = SystemTalkGroup.find_by!(system: dmr_system, talk_group: data[:talk_group]) + unless zone_system.zone_system_talkgroups.exists?(system_talk_group: stg) + ZoneSystemTalkGroup.create!(zone_system: zone_system, system_talk_group: stg) + end + end + puts "Created zone: #{dmr_zone.name} with #{dmr_zone.zone_systems.count} system(s) and #{zone_system.zone_system_talkgroups.count} talkgroups" + + # Create a public zone for sharing + public_zone = Zone.find_or_create_by!(user: user, name: "Sample Public Zone") do |z| + z.long_name = "Sample Public Zone" + z.short_name = "PUB" + z.public = true + end + puts "Created public zone: #{public_zone.name}" + + # Create a codeplug and add zones to it (new architecture) + codeplug = Codeplug.find_or_create_by!(user: user, name: "My First Codeplug") do |c| + c.description = "A sample codeplug demonstrating the zone architecture" + c.public = false + end + + # Add zones to codeplug via CodeplugZone + [ analog_zone, dmr_zone ].each_with_index do |zone, index| + CodeplugZone.find_or_create_by!(codeplug: codeplug, zone: zone) do |cz| + cz.position = index + 1 + end + end + puts "Created codeplug: #{codeplug.name} with #{codeplug.codeplug_zones.count} zone(s)" + + # Generate channels from zones + if codeplug.channels.empty? + generator = ChannelGenerator.new(codeplug) + result = generator.generate_channels + puts "Generated #{result[:channels_created]} channel(s) from #{result[:zones_processed]} zone(s)" + else + puts "Codeplug already has #{codeplug.channels.count} channel(s)" + end + puts "\n=== Seed data complete ===" puts "Login with: dev@example.com / password123" puts "Run 'rails db:seed' again to update seed data (idempotent)" diff --git a/test/controllers/channel_zones_controller_test.rb b/test/controllers/channel_zones_controller_test.rb deleted file mode 100644 index 3ec3a00..0000000 --- a/test/controllers/channel_zones_controller_test.rb +++ /dev/null @@ -1,143 +0,0 @@ -require "test_helper" - -class ChannelZonesControllerTest < ActionDispatch::IntegrationTest - setup do - @user = create(:user) - @other_user = create(:user) - @codeplug = create(:codeplug, user: @user) - @other_codeplug = create(:codeplug, user: @other_user) - @zone = create(:zone, codeplug: @codeplug, name: "Zone 1") - @other_zone = create(:zone, codeplug: @other_codeplug, name: "Other Zone") - @system = create(:system) - @channel = create(:channel, codeplug: @codeplug, system: @system, long_name: "Channel 1") - @other_channel = create(:channel, codeplug: @other_codeplug, system: @system, long_name: "Other Channel") - end - - # Create Action Tests - test "should add channel to own zone" do - log_in_as(@user) - - assert_difference("ChannelZone.count", 1) do - post codeplug_zone_channel_zones_path(@codeplug, @zone), params: { - channel_zone: { - channel_id: @channel.id - } - } - end - - channel_zone = ChannelZone.last - assert_equal @zone, channel_zone.zone - assert_equal @channel, channel_zone.channel - assert_equal 1, channel_zone.position - assert_redirected_to codeplug_zone_path(@codeplug, @zone) - assert_equal "Channel was successfully added to zone.", flash[:notice] - end - - test "should set correct position when adding channel to zone with existing channels" do - log_in_as(@user) - channel2 = create(:channel, codeplug: @codeplug, system: @system, long_name: "Channel 2") - channel3 = create(:channel, codeplug: @codeplug, system: @system, long_name: "Channel 3") - create(:channel_zone, zone: @zone, channel: @channel, position: 1) - create(:channel_zone, zone: @zone, channel: channel2, position: 2) - - assert_difference("ChannelZone.count", 1) do - post codeplug_zone_channel_zones_path(@codeplug, @zone), params: { - channel_zone: { - channel_id: channel3.id - } - } - end - - channel_zone = ChannelZone.last - assert_equal 3, channel_zone.position - end - - test "should not add channel from other user's codeplug to zone" do - log_in_as(@user) - - assert_no_difference("ChannelZone.count") do - post codeplug_zone_channel_zones_path(@codeplug, @zone), params: { - channel_zone: { - channel_id: @other_channel.id - } - } - end - - assert_response :unprocessable_entity - end - - test "should not add duplicate channel to zone" do - log_in_as(@user) - create(:channel_zone, zone: @zone, channel: @channel, position: 1) - - assert_no_difference("ChannelZone.count") do - post codeplug_zone_channel_zones_path(@codeplug, @zone), params: { - channel_zone: { - channel_id: @channel.id - } - } - end - - assert_response :unprocessable_entity - end - - test "should not add channel to other user's zone" do - log_in_as(@user) - - assert_no_difference("ChannelZone.count") do - post codeplug_zone_channel_zones_path(@other_codeplug, @other_zone), params: { - channel_zone: { - channel_id: @channel.id - } - } - end - - assert_redirected_to codeplugs_path - end - - test "should require login for create" do - assert_no_difference("ChannelZone.count") do - post codeplug_zone_channel_zones_path(@codeplug, @zone), params: { - channel_zone: { - channel_id: @channel.id - } - } - end - - assert_redirected_to login_path - end - - # Destroy Action Tests - test "should remove channel from own zone" do - log_in_as(@user) - channel_zone = create(:channel_zone, zone: @zone, channel: @channel, position: 1) - - assert_difference("ChannelZone.count", -1) do - delete codeplug_zone_channel_zone_path(@codeplug, @zone, channel_zone) - end - - assert_redirected_to codeplug_zone_path(@codeplug, @zone) - assert_equal "Channel was successfully removed from zone.", flash[:notice] - end - - test "should not remove channel from other user's zone" do - log_in_as(@user) - other_channel_zone = create(:channel_zone, zone: @other_zone, channel: @other_channel, position: 1) - - assert_no_difference("ChannelZone.count") do - delete codeplug_zone_channel_zone_path(@other_codeplug, @other_zone, other_channel_zone) - end - - assert_redirected_to codeplugs_path - end - - test "should require login for destroy" do - channel_zone = create(:channel_zone, zone: @zone, channel: @channel, position: 1) - - assert_no_difference("ChannelZone.count") do - delete codeplug_zone_channel_zone_path(@codeplug, @zone, channel_zone) - end - - assert_redirected_to login_path - end -end diff --git a/test/controllers/zones_controller_test.rb b/test/controllers/zones_controller_test.rb index a5b0896..921111b 100644 --- a/test/controllers/zones_controller_test.rb +++ b/test/controllers/zones_controller_test.rb @@ -4,93 +4,108 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest setup do @user = create(:user) @other_user = create(:user) - @codeplug = create(:codeplug, user: @user) - @other_codeplug = create(:codeplug, user: @other_user) - @zone = create(:zone, codeplug: @codeplug, name: "Zone 1") - @other_zone = create(:zone, codeplug: @other_codeplug, name: "Other Zone") + @zone = create(:zone, user: @user, name: "Zone 1") + @other_zone = create(:zone, user: @other_user, name: "Other Zone") end # Index Action Tests - test "should get index for own codeplug" do + test "should show user's zones and public zones" do log_in_as(@user) - get codeplug_zones_path(@codeplug) - assert_response :success - assert_select "h1", "Zones" - end + # Create test data + my_public_zone = create(:zone, user: @user, public: true, name: "My Public Zone") + my_private_zone = create(:zone, user: @user, public: false, name: "My Private Zone") + other_public_zone = create(:zone, user: @other_user, public: true, name: "Other Public Zone") + other_private_zone = create(:zone, user: @other_user, public: false, name: "Other Private Zone") - test "should not get index for other user's codeplug" do - log_in_as(@user) - get codeplug_zones_path(@other_codeplug) + get zones_path - assert_redirected_to codeplugs_path - assert_equal "You don't have permission to access this codeplug.", flash[:alert] + assert_response :success + # Should see own zones (both public and private) + assert_select "body", text: /My Public Zone/ + assert_select "body", text: /My Private Zone/ + # Should see other users' public zones + assert_select "body", text: /Other Public Zone/ + # Should NOT see other users' private zones + assert_select "body", { text: /Other Private Zone/, count: 0 } end test "should require login for index" do - get codeplug_zones_path(@codeplug) + get zones_path assert_redirected_to login_path end # Show Action Tests - test "should show zone for own codeplug" do + test "should display public zone to any user" do log_in_as(@user) - get codeplug_zone_path(@codeplug, @zone) + public_zone = create(:zone, user: @other_user, public: true, name: "Public Zone") + + get zone_path(public_zone) assert_response :success - assert_select "h1", @zone.name + assert_select "h1", "Public Zone" end - test "should not show zone from other user's codeplug" do + test "should display own private zone" do log_in_as(@user) - get codeplug_zone_path(@other_codeplug, @other_zone) + my_private_zone = create(:zone, user: @user, public: false, name: "My Private Zone") - assert_redirected_to codeplugs_path + get zone_path(my_private_zone) + + assert_response :success + assert_select "h1", "My Private Zone" + end + + test "should not display other user's private zone" do + log_in_as(@user) + other_private_zone = create(:zone, user: @other_user, public: false, name: "Private Zone") + + get zone_path(other_private_zone) + + assert_response :forbidden end test "should require login for show" do - get codeplug_zone_path(@codeplug, @zone) + public_zone = create(:zone, user: @user, public: true) + + get zone_path(public_zone) + assert_redirected_to login_path end # New Action Tests - test "should get new for own codeplug" do + test "should get new" do log_in_as(@user) - get new_codeplug_zone_path(@codeplug) + get new_zone_path assert_response :success assert_select "h1", "New Zone" end - test "should not get new for other user's codeplug" do - log_in_as(@user) - get new_codeplug_zone_path(@other_codeplug) - - assert_redirected_to codeplugs_path - end - test "should require login for new" do - get new_codeplug_zone_path(@codeplug) + get new_zone_path assert_redirected_to login_path end # Create Action Tests - test "should create zone for own codeplug" do + test "should create zone for logged in user" do log_in_as(@user) assert_difference("Zone.count", 1) do - post codeplug_zones_path(@codeplug), params: { + post zones_path, params: { zone: { name: "New Zone", long_name: "New Long Zone Name", - short_name: "NZN" + short_name: "NZN", + public: false } } end zone = Zone.last - assert_equal @codeplug, zone.codeplug - assert_redirected_to codeplug_zone_path(@codeplug, zone) + assert_equal @user, zone.user + assert_equal "New Zone", zone.name + assert_redirected_to zone_path(zone) assert_equal "Zone was successfully created.", flash[:notice] end @@ -98,7 +113,7 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest log_in_as(@user) assert_no_difference("Zone.count") do - post codeplug_zones_path(@codeplug), params: { + post zones_path, params: { zone: { name: "", long_name: "Test" @@ -109,23 +124,9 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest assert_response :unprocessable_entity end - test "should not create zone for other user's codeplug" do - log_in_as(@user) - - assert_no_difference("Zone.count") do - post codeplug_zones_path(@other_codeplug), params: { - zone: { - name: "Hacked Zone" - } - } - end - - assert_redirected_to codeplugs_path - end - test "should require login for create" do assert_no_difference("Zone.count") do - post codeplug_zones_path(@codeplug), params: { + post zones_path, params: { zone: { name: "Test" } @@ -138,7 +139,7 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest # Edit Action Tests test "should get edit for own zone" do log_in_as(@user) - get edit_codeplug_zone_path(@codeplug, @zone) + get edit_zone_path(@zone) assert_response :success assert_select "h1", text: /Edit Zone/ @@ -146,13 +147,13 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest test "should not get edit for other user's zone" do log_in_as(@user) - get edit_codeplug_zone_path(@other_codeplug, @other_zone) + get edit_zone_path(@other_zone) - assert_redirected_to codeplugs_path + assert_response :forbidden end test "should require login for edit" do - get edit_codeplug_zone_path(@codeplug, @zone) + get edit_zone_path(@zone) assert_redirected_to login_path end @@ -160,7 +161,7 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest test "should update own zone" do log_in_as(@user) - patch codeplug_zone_path(@codeplug, @zone), params: { + patch zone_path(@zone), params: { zone: { name: "Updated Zone", long_name: "Updated Long Name" @@ -170,14 +171,14 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest @zone.reload assert_equal "Updated Zone", @zone.name assert_equal "Updated Long Name", @zone.long_name - assert_redirected_to codeplug_zone_path(@codeplug, @zone) + assert_redirected_to zone_path(@zone) assert_equal "Zone was successfully updated.", flash[:notice] end test "should not update without name" do log_in_as(@user) - patch codeplug_zone_path(@codeplug, @zone), params: { + patch zone_path(@zone), params: { zone: { name: "" } @@ -190,7 +191,7 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest log_in_as(@user) original_name = @other_zone.name - patch codeplug_zone_path(@other_codeplug, @other_zone), params: { + patch zone_path(@other_zone), params: { zone: { name: "Hacked Name" } @@ -198,11 +199,11 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest @other_zone.reload assert_equal original_name, @other_zone.name - assert_redirected_to codeplugs_path + assert_response :forbidden end test "should require login for update" do - patch codeplug_zone_path(@codeplug, @zone), params: { + patch zone_path(@zone), params: { zone: { name: "New Name" } @@ -216,10 +217,10 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest log_in_as(@user) assert_difference("Zone.count", -1) do - delete codeplug_zone_path(@codeplug, @zone) + delete zone_path(@zone) end - assert_redirected_to codeplug_zones_path(@codeplug) + assert_redirected_to zones_path assert_equal "Zone was successfully deleted.", flash[:notice] end @@ -227,222 +228,67 @@ class ZonesControllerTest < ActionDispatch::IntegrationTest log_in_as(@user) assert_no_difference("Zone.count") do - delete codeplug_zone_path(@other_codeplug, @other_zone) + delete zone_path(@other_zone) end - assert_redirected_to codeplugs_path + assert_response :forbidden end test "should require login for destroy" do assert_no_difference("Zone.count") do - delete codeplug_zone_path(@codeplug, @zone) + delete zone_path(@zone) end assert_redirected_to login_path end - # Update Positions Action Tests - test "should update channel positions for own zone" do + # Update Positions Action Tests (for zone_systems reordering) + test "should update zone_system positions for own zone" do log_in_as(@user) - system = create(:system) - channel1 = create(:channel, codeplug: @codeplug, system: system, long_name: "Channel 1") - channel2 = create(:channel, codeplug: @codeplug, system: system, long_name: "Channel 2") - channel3 = create(:channel, codeplug: @codeplug, system: system, long_name: "Channel 3") + system1 = create(:system, :analog, name: "System 1") + system2 = create(:system, :analog, name: "System 2") + system3 = create(:system, :analog, name: "System 3") - cz1 = create(:channel_zone, zone: @zone, channel: channel1, position: 1) - cz2 = create(:channel_zone, zone: @zone, channel: channel2, position: 2) - cz3 = create(:channel_zone, zone: @zone, channel: channel3, position: 3) + zs1 = create(:zone_system, zone: @zone, system: system1, position: 1) + zs2 = create(:zone_system, zone: @zone, system: system2, position: 2) + zs3 = create(:zone_system, zone: @zone, system: system3, position: 3) - # Reorder: move channel3 to position 1 - patch update_positions_codeplug_zone_path(@codeplug, @zone), params: { + # Reorder: move system3 to position 1 + patch update_positions_zone_path(@zone), params: { positions: [ - { id: cz3.id, position: 1 }, - { id: cz1.id, position: 2 }, - { id: cz2.id, position: 3 } + { id: zs3.id, position: 1 }, + { id: zs1.id, position: 2 }, + { id: zs2.id, position: 3 } ] }, as: :json assert_response :success # Verify positions updated - assert_equal 1, cz3.reload.position - assert_equal 2, cz1.reload.position - assert_equal 3, cz2.reload.position + assert_equal 1, zs3.reload.position + assert_equal 2, zs1.reload.position + assert_equal 3, zs2.reload.position end test "should not update positions for other user's zone" do log_in_as(@user) - system = create(:system) - channel = create(:channel, codeplug: @other_codeplug, system: system) - cz = create(:channel_zone, zone: @other_zone, channel: channel, position: 1) + system = create(:system, :analog) + zs = create(:zone_system, zone: @other_zone, system: system, position: 1) - patch update_positions_codeplug_zone_path(@other_codeplug, @other_zone), params: { + patch update_positions_zone_path(@other_zone), params: { positions: [ - { id: cz.id, position: 2 } + { id: zs.id, position: 2 } ] }, as: :json - assert_redirected_to codeplugs_path + assert_response :forbidden end test "should require login for update_positions" do - patch update_positions_codeplug_zone_path(@codeplug, @zone), params: { + patch update_positions_zone_path(@zone), params: { positions: [] }, as: :json assert_redirected_to login_path end - - # Standalone Zones Routes Tests (new top-level resource) - # These tests are for the new standalone zones functionality (not nested under codeplugs) - - test "standalone index should show user's zones and public zones" do - log_in_as(@user) - - # Create test data - my_public_zone = create(:zone, user: @user, public: true, name: "My Public Zone") - my_private_zone = create(:zone, user: @user, public: false, name: "My Private Zone") - other_public_zone = create(:zone, user: @other_user, public: true, name: "Other Public Zone") - other_private_zone = create(:zone, user: @other_user, public: false, name: "Other Private Zone") - - get zones_path - - assert_response :success - # Should see own zones (both public and private) - assert_select "body", text: /My Public Zone/ - assert_select "body", text: /My Private Zone/ - # Should see other users' public zones - assert_select "body", text: /Other Public Zone/ - # Should NOT see other users' private zones - assert_select "body", { text: /Other Private Zone/, count: 0 } - end - - test "standalone index should require login" do - get zones_path - assert_redirected_to login_path - end - - test "standalone show should display public zone to any user" do - log_in_as(@user) - public_zone = create(:zone, user: @other_user, public: true, name: "Public Zone") - - get zone_path(public_zone) - - assert_response :success - assert_select "h1", "Public Zone" - end - - test "standalone show should display own private zone" do - log_in_as(@user) - my_private_zone = create(:zone, user: @user, public: false, name: "My Private Zone") - - get zone_path(my_private_zone) - - assert_response :success - assert_select "h1", "My Private Zone" - end - - test "standalone show should not display other user's private zone" do - log_in_as(@user) - other_private_zone = create(:zone, user: @other_user, public: false, name: "Private Zone") - - get zone_path(other_private_zone) - - assert_response :forbidden - end - - test "standalone show should require login" do - public_zone = create(:zone, user: @user, public: true) - - get zone_path(public_zone) - - assert_redirected_to login_path - end - - test "standalone new should require login" do - get new_zone_path - assert_redirected_to login_path - end - - test "standalone create should create zone for logged in user" do - log_in_as(@user) - - assert_difference("Zone.count", 1) do - post zones_path, params: { - zone: { - name: "New Standalone Zone", - long_name: "New Standalone Zone Long", - short_name: "NSZ", - public: false - } - } - end - - zone = Zone.last - assert_equal @user, zone.user - assert_equal "New Standalone Zone", zone.name - assert_redirected_to zone_path(zone) - end - - test "standalone create should require login" do - assert_no_difference("Zone.count") do - post zones_path, params: { - zone: { - name: "New Zone" - } - } - end - - assert_redirected_to login_path - end - - test "standalone update should update own zone" do - log_in_as(@user) - my_zone = create(:zone, user: @user, name: "Original Name") - - patch zone_path(my_zone), params: { - zone: { - name: "Updated Name" - } - } - - assert_equal "Updated Name", my_zone.reload.name - assert_redirected_to zone_path(my_zone) - end - - test "standalone update should not update other user's zone" do - log_in_as(@user) - other_zone = create(:zone, user: @other_user, name: "Original Name") - - patch zone_path(other_zone), params: { - zone: { - name: "Hacked Name" - } - } - - assert_equal "Original Name", other_zone.reload.name - assert_response :forbidden - end - - test "standalone destroy should delete own zone" do - log_in_as(@user) - my_zone = create(:zone, user: @user) - - assert_difference("Zone.count", -1) do - delete zone_path(my_zone) - end - - assert_redirected_to zones_path - end - - test "standalone destroy should not delete other user's zone" do - log_in_as(@user) - other_zone = create(:zone, user: @other_user) - - assert_no_difference("Zone.count") do - delete zone_path(other_zone) - end - - assert_response :forbidden - end end diff --git a/test/factories/codeplug_zones.rb b/test/factories/codeplug_zones.rb index 4493850..bbd2225 100644 --- a/test/factories/codeplug_zones.rb +++ b/test/factories/codeplug_zones.rb @@ -3,10 +3,5 @@ association :codeplug association :zone sequence(:position) { |n| n } - - # Ensure zone belongs to the same codeplug - after(:build) do |cz| - cz.zone.codeplug = cz.codeplug if cz.codeplug && cz.zone - end end end diff --git a/test/factories/zones.rb b/test/factories/zones.rb index 9de62a5..40365d9 100644 --- a/test/factories/zones.rb +++ b/test/factories/zones.rb @@ -1,7 +1,6 @@ FactoryBot.define do factory :zone do association :user - association :codeplug name { Faker::Lorem.words(number: 2).join(" ").titleize } long_name { Faker::Lorem.words(number: 4).join(" ").titleize } short_name { Faker::Alphanumeric.alpha(number: 3).upcase } diff --git a/test/models/codeplug_test.rb b/test/models/codeplug_test.rb index b45e83b..302a3b9 100644 --- a/test/models/codeplug_test.rb +++ b/test/models/codeplug_test.rb @@ -54,15 +54,22 @@ class CodeplugTest < ActiveSupport::TestCase assert_equal :belongs_to, association.macro end - test "should have many zones" do + test "should have many zones through codeplug_zones" do codeplug = create(:codeplug) assert_respond_to codeplug, :zones end - test "zones association should be configured with dependent destroy" do + test "zones association should be configured as through" do association = Codeplug.reflect_on_association(:zones) assert_not_nil association, "zones association should exist" assert_equal :has_many, association.macro + assert_equal :codeplug_zones, association.options[:through] + end + + test "should have many codeplug_zones with dependent destroy" do + association = Codeplug.reflect_on_association(:codeplug_zones) + assert_not_nil association, "codeplug_zones association should exist" + assert_equal :has_many, association.macro assert_equal :destroy, association.options[:dependent] end @@ -79,11 +86,17 @@ class CodeplugTest < ActiveSupport::TestCase end # Dependent Destroy Tests - test "destroying codeplug should destroy associated zones" do - codeplug = create(:codeplug) - # Note: Zones will be created in a later issue, so this test will fail until then - # For now, we're just testing the association configuration exists - skip "Zone model not yet implemented" + test "destroying codeplug should destroy associated codeplug_zones" do + user = create(:user) + codeplug = create(:codeplug, user: user) + zone = create(:zone, user: user) + codeplug_zone = create(:codeplug_zone, codeplug: codeplug, zone: zone, position: 1) + + assert_difference("CodeplugZone.count", -1) do + codeplug.destroy! + end + # Zone itself should still exist (it's standalone) + assert Zone.exists?(zone.id) end test "destroying codeplug should destroy associated channels" do diff --git a/test/models/codeplug_zone_test.rb b/test/models/codeplug_zone_test.rb index 9180d10..73698ba 100644 --- a/test/models/codeplug_zone_test.rb +++ b/test/models/codeplug_zone_test.rb @@ -73,8 +73,9 @@ class CodeplugZoneTest < ActiveSupport::TestCase # Uniqueness Tests test "should not save codeplug_zone with duplicate zone in same codeplug" do - codeplug = create(:codeplug) - zone = create(:zone, codeplug: codeplug) + user = create(:user) + codeplug = create(:codeplug, user: user) + zone = create(:zone, user: user) create(:codeplug_zone, codeplug: codeplug, zone: zone, position: 1) duplicate = build(:codeplug_zone, codeplug: codeplug, zone: zone, position: 2) @@ -83,21 +84,22 @@ class CodeplugZoneTest < ActiveSupport::TestCase end test "should save codeplug_zone with same zone in different codeplugs" do - codeplug1 = create(:codeplug) - codeplug2 = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug1) - zone2 = create(:zone, codeplug: codeplug2) + user = create(:user) + codeplug1 = create(:codeplug, user: user) + codeplug2 = create(:codeplug, user: user) + zone = create(:zone, user: user) - cz1 = create(:codeplug_zone, codeplug: codeplug1, zone: zone1, position: 1) - cz2 = build(:codeplug_zone, codeplug: codeplug2, zone: zone2, position: 1) + cz1 = create(:codeplug_zone, codeplug: codeplug1, zone: zone, position: 1) + cz2 = build(:codeplug_zone, codeplug: codeplug2, zone: zone, position: 1) - assert cz2.save, "Failed to save same zone in different codeplug" + assert cz2.save, "Failed to save same zone in different codeplugs" end test "should not save codeplug_zone with duplicate position in same codeplug" do - codeplug = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug, name: "Zone 1") - zone2 = create(:zone, codeplug: codeplug, name: "Zone 2") + user = create(:user) + codeplug = create(:codeplug, user: user) + zone1 = create(:zone, user: user, name: "Zone 1") + zone2 = create(:zone, user: user, name: "Zone 2") create(:codeplug_zone, codeplug: codeplug, zone: zone1, position: 1) duplicate = build(:codeplug_zone, codeplug: codeplug, zone: zone2, position: 1) @@ -106,10 +108,11 @@ class CodeplugZoneTest < ActiveSupport::TestCase end test "should save codeplug_zone with same position in different codeplugs" do - codeplug1 = create(:codeplug) - codeplug2 = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug1) - zone2 = create(:zone, codeplug: codeplug2) + user = create(:user) + codeplug1 = create(:codeplug, user: user) + codeplug2 = create(:codeplug, user: user) + zone1 = create(:zone, user: user) + zone2 = create(:zone, user: user) cz1 = create(:codeplug_zone, codeplug: codeplug1, zone: zone1, position: 1) cz2 = build(:codeplug_zone, codeplug: codeplug2, zone: zone2, position: 1) @@ -125,10 +128,11 @@ class CodeplugZoneTest < ActiveSupport::TestCase # Multiple CodeplugZones per Codeplug test "codeplug can have multiple codeplug_zones at different positions" do - codeplug = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug, name: "Zone 1") - zone2 = create(:zone, codeplug: codeplug, name: "Zone 2") - zone3 = create(:zone, codeplug: codeplug, name: "Zone 3") + user = create(:user) + codeplug = create(:codeplug, user: user) + zone1 = create(:zone, user: user, name: "Zone 1") + zone2 = create(:zone, user: user, name: "Zone 2") + zone3 = create(:zone, user: user, name: "Zone 3") cz1 = create(:codeplug_zone, codeplug: codeplug, zone: zone1, position: 1) cz2 = create(:codeplug_zone, codeplug: codeplug, zone: zone2, position: 2) @@ -142,10 +146,11 @@ class CodeplugZoneTest < ActiveSupport::TestCase # Default Scope - Ordering by Position test "codeplug_zones should be ordered by position by default" do - codeplug = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug, name: "Zone 1") - zone2 = create(:zone, codeplug: codeplug, name: "Zone 2") - zone3 = create(:zone, codeplug: codeplug, name: "Zone 3") + user = create(:user) + codeplug = create(:codeplug, user: user) + zone1 = create(:zone, user: user, name: "Zone 1") + zone2 = create(:zone, user: user, name: "Zone 2") + zone3 = create(:zone, user: user, name: "Zone 3") # Create in non-sequential order cz3 = create(:codeplug_zone, codeplug: codeplug, zone: zone3, position: 3) @@ -158,9 +163,10 @@ class CodeplugZoneTest < ActiveSupport::TestCase # Through Association Tests test "codeplug should have zones through codeplug_zones" do - codeplug = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug, name: "Zone 1") - zone2 = create(:zone, codeplug: codeplug, name: "Zone 2") + user = create(:user) + codeplug = create(:codeplug, user: user) + zone1 = create(:zone, user: user, name: "Zone 1") + zone2 = create(:zone, user: user, name: "Zone 2") create(:codeplug_zone, codeplug: codeplug, zone: zone1, position: 1) create(:codeplug_zone, codeplug: codeplug, zone: zone2, position: 2) @@ -171,9 +177,10 @@ class CodeplugZoneTest < ActiveSupport::TestCase end test "zone should have codeplugs through codeplug_zones" do - zone = create(:zone) - codeplug1 = create(:codeplug) - codeplug2 = create(:codeplug) + user = create(:user) + zone = create(:zone, user: user) + codeplug1 = create(:codeplug, user: user) + codeplug2 = create(:codeplug, user: user) create(:codeplug_zone, zone: zone, codeplug: codeplug1, position: 1) create(:codeplug_zone, zone: zone, codeplug: codeplug2, position: 1) diff --git a/test/models/zone_test.rb b/test/models/zone_test.rb index 0a70f25..ec28955 100644 --- a/test/models/zone_test.rb +++ b/test/models/zone_test.rb @@ -7,11 +7,6 @@ class ZoneTest < ActiveSupport::TestCase assert zone.save, "Failed to save zone with valid attributes" end - test "should save zone without codeplug (codeplug is now optional)" do - zone = build(:zone, codeplug: nil) - assert zone.save, "Failed to save zone without codeplug" - end - test "should not save zone without user" do zone = build(:zone, user: nil) assert_not zone.save, "Saved zone without user" @@ -35,17 +30,6 @@ class ZoneTest < ActiveSupport::TestCase end # Association Tests - test "should belong to codeplug" do - zone = build(:zone) - assert_respond_to zone, :codeplug - end - - test "codeplug association should be configured" do - association = Zone.reflect_on_association(:codeplug) - assert_not_nil association, "codeplug association should exist" - assert_equal :belongs_to, association.macro - end - test "should have many channel_zones" do zone = create(:zone) assert_respond_to zone, :channel_zones @@ -70,12 +54,45 @@ class ZoneTest < ActiveSupport::TestCase assert_equal :channel_zones, association.options[:through] end - # Dependent Destroy Tests - test "destroying zone should destroy associated channel_zones" do + test "should have many codeplug_zones" do zone = create(:zone) - # Note: ChannelZone will be created in a later issue, so this test will fail until then - # For now, we're just testing the association configuration exists - skip "ChannelZone model not yet implemented" + assert_respond_to zone, :codeplug_zones + end + + test "codeplug_zones association should be configured with dependent destroy" do + association = Zone.reflect_on_association(:codeplug_zones) + assert_not_nil association, "codeplug_zones association should exist" + assert_equal :has_many, association.macro + assert_equal :destroy, association.options[:dependent] + end + + test "should have many codeplugs through codeplug_zones" do + zone = create(:zone) + assert_respond_to zone, :codeplugs + end + + test "codeplugs association should be configured as through" do + association = Zone.reflect_on_association(:codeplugs) + assert_not_nil association, "codeplugs association should exist" + assert_equal :has_many, association.macro + assert_equal :codeplug_zones, association.options[:through] + end + + test "should have many zone_systems" do + zone = create(:zone) + assert_respond_to zone, :zone_systems + end + + test "zone_systems association should be configured with dependent destroy" do + association = Zone.reflect_on_association(:zone_systems) + assert_not_nil association, "zone_systems association should exist" + assert_equal :has_many, association.macro + assert_equal :destroy, association.options[:dependent] + end + + test "should have many systems through zone_systems" do + zone = create(:zone) + assert_respond_to zone, :systems end # Attribute Tests @@ -94,27 +111,6 @@ class ZoneTest < ActiveSupport::TestCase assert_equal "RPT", zone.short_name end - test "should allow different zones with same name in different codeplugs" do - codeplug1 = create(:codeplug) - codeplug2 = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug1, name: "Zone A") - zone2 = create(:zone, codeplug: codeplug2, name: "Zone A") - - assert zone1.persisted? - assert zone2.persisted? - end - - # Multiple Zones per Codeplug - test "codeplug can have multiple zones" do - codeplug = create(:codeplug) - zone1 = create(:zone, codeplug: codeplug, name: "Zone 1") - zone2 = create(:zone, codeplug: codeplug, name: "Zone 2") - - assert_equal 2, codeplug.zones.count - assert_includes codeplug.zones, zone1 - assert_includes codeplug.zones, zone2 - end - # Name Length Tests (no validation, just storage) test "should store long names" do long_name = "A" * 100 @@ -270,10 +266,32 @@ class ZoneTest < ActiveSupport::TestCase assert zone2.persisted? end - # Codeplug Optional Tests - test "zone can exist without being associated to a codeplug" do - zone = create(:zone, codeplug: nil) - assert_nil zone.codeplug - assert zone.persisted? + # Codeplug Relationship Tests (through CodeplugZone) + test "zone can be added to multiple codeplugs" do + user = create(:user) + zone = create(:zone, user: user) + codeplug1 = create(:codeplug, user: user) + codeplug2 = create(:codeplug, user: user) + + create(:codeplug_zone, codeplug: codeplug1, zone: zone, position: 1) + create(:codeplug_zone, codeplug: codeplug2, zone: zone, position: 1) + + assert_equal 2, zone.codeplugs.count + assert_includes zone.codeplugs, codeplug1 + assert_includes zone.codeplugs, codeplug2 + end + + test "codeplug can have multiple zones" do + user = create(:user) + codeplug = create(:codeplug, user: user) + zone1 = create(:zone, user: user, name: "Zone 1") + zone2 = create(:zone, user: user, name: "Zone 2") + + create(:codeplug_zone, codeplug: codeplug, zone: zone1, position: 1) + create(:codeplug_zone, codeplug: codeplug, zone: zone2, position: 2) + + assert_equal 2, codeplug.zones.count + assert_includes codeplug.zones, zone1 + assert_includes codeplug.zones, zone2 end end diff --git a/test/system/zones_test.rb b/test/system/zones_test.rb index 5b10d26..5dd41c7 100644 --- a/test/system/zones_test.rb +++ b/test/system/zones_test.rb @@ -1,227 +1,7 @@ require "application_system_test_case" class ZonesTest < ApplicationSystemTestCase - test "creating a new zone" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - - visit codeplug_path(codeplug) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - click_link "Manage Zones" - click_link "New Zone" - - fill_in "Name", with: "Zone 1" - fill_in "Long name", with: "Local Repeaters" - fill_in "Short name", with: "LCL" - click_button "Create Zone" - - assert_text "Zone was successfully created" - assert_text "Zone 1" - assert_text "Local Repeaters" - assert_text "LCL" - end - - test "editing a zone" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone = create(:zone, codeplug: codeplug, name: "Original Zone") - - visit codeplug_zones_path(codeplug) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - click_link "Edit", match: :first - - fill_in "Name", with: "Updated Zone" - fill_in "Long name", with: "Updated Long Name" - click_button "Update Zone" - - assert_text "Zone was successfully updated" - assert_text "Updated Zone" - assert_text "Updated Long Name" - end - - test "viewing zones index" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone1 = create(:zone, codeplug: codeplug, name: "Zone 1", long_name: "Zone One") - zone2 = create(:zone, codeplug: codeplug, name: "Zone 2", long_name: "Zone Two") - - visit codeplug_zones_path(codeplug) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - assert_text "Zone 1" - assert_text "Zone 2" - assert_text "Zone One" - assert_text "Zone Two" - end - - test "viewing zone details" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone = create(:zone, codeplug: codeplug, name: "Test Zone", long_name: "Test Long Name") - - visit codeplug_zone_path(codeplug, zone) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - assert_text "Test Zone" - assert_text "Test Long Name" - end - - test "empty state shows helpful message" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - - visit codeplug_zones_path(codeplug) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - assert_text "No zones found" - assert_link "Create the first one" - end - - test "manage zones link from codeplug show page" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - - visit codeplug_path(codeplug) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - click_link "Manage Zones" - - assert_current_path codeplug_zones_path(codeplug) - assert_text "Zones" - end - - test "cannot access other user's zones" do - user = create(:user, email: "test@example.com", password: "password123") - other_user = create(:user) - other_codeplug = create(:codeplug, user: other_user, name: "Other Codeplug") - - visit codeplug_zones_path(other_codeplug) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - assert_text "You don't have permission to access this codeplug" - assert_current_path codeplugs_path - end - - test "zone shows channels with drag handles" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone = create(:zone, codeplug: codeplug, name: "Test Zone") - system = create(:system) - channel1 = create(:channel, codeplug: codeplug, system: system, long_name: "Channel 1") - channel2 = create(:channel, codeplug: codeplug, system: system, long_name: "Channel 2") - create(:channel_zone, zone: zone, channel: channel1, position: 1) - create(:channel_zone, zone: zone, channel: channel2, position: 2) - - visit codeplug_zone_path(codeplug, zone) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - assert_text "Channel 1" - assert_text "Channel 2" - assert_text "2 channels" - - # Verify drag handles are present (via Bootstrap icon SVG) - assert_selector ".drag-handle", count: 2 - assert_selector ".list-group-item[data-id]", count: 2 - end - - test "adding a channel to a zone" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone = create(:zone, codeplug: codeplug, name: "Test Zone") - system = create(:system) - channel = create(:channel, codeplug: codeplug, system: system, long_name: "Test Channel") - - visit codeplug_zone_path(codeplug, zone) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - # Verify the add channel form is present - assert_selector "select#channel_zone_channel_id" - assert_button "Add Channel" - - select "Test Channel", from: "channel_zone_channel_id" - click_button "Add Channel" - - assert_text "Channel was successfully added to zone" - assert_text "Test Channel" - assert_text "1 channel" - end - - # Skipping due to Turbo confirm dialog issues with Capybara - # Controller tests cover the functionality - test "removing a channel from a zone" do - skip "Turbo confirm dialogs don't work reliably in system tests" - - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone = create(:zone, codeplug: codeplug, name: "Test Zone") - system = create(:system) - channel = create(:channel, codeplug: codeplug, system: system, long_name: "Test Channel") - create(:channel_zone, zone: zone, channel: channel, position: 1) - - visit codeplug_zone_path(codeplug, zone) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - assert_text "Test Channel" - assert_text "1 channel" - - click_button "Remove" - - assert_text "Channel was successfully removed from zone" - assert_text "No channels in this zone yet" - end - - test "editing a channel from zone view" do - user = create(:user, email: "test@example.com", password: "password123") - codeplug = create(:codeplug, user: user, name: "Test Codeplug") - zone = create(:zone, codeplug: codeplug, name: "Test Zone") - system = create(:system) - channel = create(:channel, codeplug: codeplug, system: system, long_name: "Original Name") - create(:channel_zone, zone: zone, channel: channel, position: 1) - - visit codeplug_zone_path(codeplug, zone) - fill_in "Email", with: "test@example.com" - fill_in "Password", with: "password123" - click_button "Log In" - - # Click the Edit button within the channel list (not the zone edit button) - within(".list-group") do - click_link "Edit" - end - - assert_text "Edit Channel" - fill_in "Long name", with: "Updated Name" - click_button "Update Channel" - - assert_text "Channel was successfully updated" - end - - # ======================================== - # Standalone Zones Tests (top-level resource) - # ======================================== - - test "user can navigate to standalone zones index from navbar" do + test "user can navigate to zones index from navbar" do user = create(:user, email: "test@example.com", password: "password123") create(:zone, user: user, name: "My Zone", public: false) @@ -237,7 +17,7 @@ class ZonesTest < ApplicationSystemTestCase assert_text "My Zone" end - test "standalone zones index shows user's zones and public zones" do + test "zones index shows user's zones and public zones" do user = create(:user, email: "test@example.com", password: "password123") other_user = create(:user, email: "other@example.com") @@ -263,7 +43,7 @@ class ZonesTest < ApplicationSystemTestCase assert_no_text "Other Private Zone" end - test "standalone zones index shows owner and visibility badges" do + test "zones index shows owner and visibility badges" do user = create(:user, email: "test@example.com", password: "password123") other_user = create(:user, email: "other@example.com") @@ -275,7 +55,7 @@ class ZonesTest < ApplicationSystemTestCase fill_in "Password", with: "password123" click_button "Log In" - # Verify the table headers for standalone zones + # Verify the table headers assert_selector "th", text: "Owner" assert_selector "th", text: "Visibility" assert_selector "th", text: "Systems" @@ -287,7 +67,7 @@ class ZonesTest < ApplicationSystemTestCase assert_text "other@example.com" # Owner column for other user's zone end - test "user can view standalone zone details" do + test "user can view zone details" do user = create(:user, email: "test@example.com", password: "password123") zone = create(:zone, user: user, name: "Test Zone", long_name: "Test Long Name", public: false) @@ -336,7 +116,7 @@ class ZonesTest < ApplicationSystemTestCase assert_no_text "Other Private Zone" end - test "standalone zone shows systems with drag-drop reordering" do + test "zone shows systems with drag-drop reordering" do user = create(:user, email: "test@example.com", password: "password123") zone = create(:zone, user: user, name: "Test Zone") system1 = create(:system, name: "System 1", rx_frequency: 145.0, tx_frequency: 145.6) @@ -360,7 +140,7 @@ class ZonesTest < ApplicationSystemTestCase assert_selector ".list-group-item[data-id]", count: 2 end - test "adding a system to standalone zone" do + test "adding a system to zone" do user = create(:user, email: "test@example.com", password: "password123") zone = create(:zone, user: user, name: "Test Zone") system = create(:system, name: "Test System", rx_frequency: 145.0, tx_frequency: 145.6) @@ -382,7 +162,7 @@ class ZonesTest < ApplicationSystemTestCase assert_text "1 system" end - test "standalone zone empty state shows helpful message" do + test "zone empty state shows helpful message" do user = create(:user, email: "test@example.com", password: "password123") zone = create(:zone, user: user, name: "Empty Zone") @@ -394,7 +174,7 @@ class ZonesTest < ApplicationSystemTestCase assert_text "No systems in this zone yet" end - test "creating a new standalone zone" do + test "creating a new zone" do user = create(:user, email: "test@example.com", password: "password123") visit new_zone_path @@ -402,17 +182,17 @@ class ZonesTest < ApplicationSystemTestCase fill_in "Password", with: "password123" click_button "Log In" - fill_in "Name", with: "New Standalone Zone" + fill_in "Name", with: "New Zone" fill_in "Long name", with: "My New Zone Long Name" - fill_in "Short name", with: "NSZ" + fill_in "Short name", with: "NZ" click_button "Create Zone" assert_text "Zone was successfully created" - assert_text "New Standalone Zone" + assert_text "New Zone" assert_text "My New Zone Long Name" end - test "editing standalone zone" do + test "editing zone" do user = create(:user, email: "test@example.com", password: "password123") zone = create(:zone, user: user, name: "Original Name") @@ -430,6 +210,18 @@ class ZonesTest < ApplicationSystemTestCase assert_text "Updated Long Name" end + test "zones index empty state shows helpful message" do + user = create(:user, email: "test@example.com", password: "password123") + + visit zones_path + fill_in "Email", with: "test@example.com" + fill_in "Password", with: "password123" + click_button "Log In" + + assert_text "No zones found" + assert_link "Create the first one" + end + # Talkgroup selection tests for digital systems test "digital system in zone shows talkgroup management" do user = create(:user, email: "test@example.com", password: "password123") From eab980eae6d050f896ac3afb6ac39e238c9e0603 Mon Sep 17 00:00:00 2001 From: Andrew Ohnstad Date: Wed, 17 Dec 2025 20:29:53 -0500 Subject: [PATCH 2/2] Fix codeplugs system test to use new zone architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update test_viewing_codeplug_shows_zones_and_channels_summary to create zones with the standalone pattern (owned by user, linked via CodeplugZone) instead of the old direct codeplug association. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- test/system/codeplugs_test.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/system/codeplugs_test.rb b/test/system/codeplugs_test.rb index 05ad1e4..036104a 100644 --- a/test/system/codeplugs_test.rb +++ b/test/system/codeplugs_test.rb @@ -85,9 +85,13 @@ class CodeplugsTest < ApplicationSystemTestCase user = create(:user, email: "test@example.com", password: "password123") codeplug = create(:codeplug, user: user, name: "Test Codeplug") - # Create some zones and channels - zone1 = create(:zone, codeplug: codeplug, long_name: "Zone 1") - zone2 = create(:zone, codeplug: codeplug, long_name: "Zone 2") + # Create standalone zones and add to codeplug via CodeplugZone + zone1 = create(:zone, user: user, name: "Zone 1", long_name: "Zone 1") + zone2 = create(:zone, user: user, name: "Zone 2", long_name: "Zone 2") + create(:codeplug_zone, codeplug: codeplug, zone: zone1, position: 1) + create(:codeplug_zone, codeplug: codeplug, zone: zone2, position: 2) + + # Create channels system = create(:system, mode: "dmr") channel1 = create(:channel, codeplug: codeplug, system: system, long_name: "Channel 1") channel2 = create(:channel, codeplug: codeplug, system: system, long_name: "Channel 2")
Name Long Name Short NameOwnerVisibilitySystemsChannelsOwnerVisibilitySystems Actions
<%= link_to zone.name, codeplug_zone_path(@codeplug, zone) %><%= link_to zone.name, zone_path(zone) %><%= link_to zone.name, zone_path(zone) %> <%= zone.long_name || "—" %> <%= zone.short_name || "—" %><%= zone.channels.count %> - <% if zone.user == current_user %> - You - <% else %> - <%= zone.user.email %> - <% end %> - - <% if zone.public? %> - Public - <% else %> - Private - <% end %> - <%= zone.zone_systems.count %> + <% if zone.user == current_user %> + You + <% else %> + <%= zone.user.email %> + <% end %> + + <% if zone.public? %> + Public + <% else %> + Private + <% end %> + <%= zone.zone_systems.count %> - <% if @codeplug %> - <%= link_to "View", codeplug_zone_path(@codeplug, zone), class: "btn btn-sm btn-info me-1" %> - <%= link_to "Edit", edit_codeplug_zone_path(@codeplug, zone), class: "btn btn-sm btn-secondary me-1" %> - <%= button_to "Delete", codeplug_zone_path(@codeplug, zone), method: :delete, + <%= link_to "View", zone_path(zone), class: "btn btn-sm btn-info me-1" %> + <% if zone.editable_by?(current_user) %> + <%= link_to "Edit", edit_zone_path(zone), class: "btn btn-sm btn-secondary me-1" %> + <%= button_to "Delete", zone_path(zone), method: :delete, class: "btn btn-sm btn-danger", form: { class: "d-inline" }, data: { turbo_confirm: "Are you sure?" } %> - <% else %> - <%= link_to "View", zone_path(zone), class: "btn btn-sm btn-info me-1" %> - <% if zone.editable_by?(current_user) %> - <%= link_to "Edit", edit_zone_path(zone), class: "btn btn-sm btn-secondary me-1" %> - <%= button_to "Delete", zone_path(zone), method: :delete, - class: "btn btn-sm btn-danger", - form: { class: "d-inline" }, - data: { turbo_confirm: "Are you sure?" } %> - <% end %> <% end %>