Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions app/controllers/organization_alliances_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
class OrganizationAlliancesController < ApplicationController
before_action :authenticate_user!
before_action :member_should_exist_and_be_active
before_action :authorize_admin
before_action :find_alliance, only: [:update, :destroy]

def index
@status = params[:status] || "pending"

@alliances = case @status
when "pending"
current_organization.pending_sent_alliances.includes(:source_organization, :target_organization) +
current_organization.pending_received_alliances.includes(:source_organization, :target_organization)
when "accepted"
current_organization.accepted_alliances.includes(:source_organization, :target_organization)
when "rejected"
current_organization.rejected_alliances.includes(:source_organization, :target_organization)
else
[]
end
end

def create
@alliance = OrganizationAlliance.new(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no need for @ivar here I think

source_organization: current_organization,
target_organization_id: params[:organization_alliance][:target_organization_id],
status: "pending"
Copy link
Preview

Copilot AI Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the create action, the target_organization_id is accessed directly from params instead of using the alliance_params method. Consider refactoring to use strong parameters (alliance_params) to enhance consistency and security.

Suggested change
target_organization_id: params[:organization_alliance][:target_organization_id],
status: "pending"
target_organization_id: alliance_params[:target_organization_id],

Copilot uses AI. Check for mistakes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gmartincor another point for Copilot

)

if @alliance.save
flash[:notice] = t("organization_alliances.created")
else
flash[:error] = @alliance.errors.full_messages.to_sentence
end

redirect_back fallback_location: organizations_path
end

def update
authorize @alliance
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we have before_action :authorize_admin we don't need an authorize here


if @alliance.update(status: params[:status])
flash[:notice] = t("organization_alliances.updated")
else
flash[:error] = @alliance.errors.full_messages.to_sentence
end

redirect_to organization_alliances_path
end

def destroy
authorize @alliance
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here


if @alliance.destroy
flash[:notice] = t("organization_alliances.destroyed")
else
flash[:error] = t("organization_alliances.error_destroying")
end

redirect_to organization_alliances_path
end

private

def find_alliance
@alliance = OrganizationAlliance.find(params[:id])
end

def authorize_admin
unless current_user.manages?(current_organization)
flash[:error] = t("organization_alliances.not_authorized")
redirect_to root_path
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code could be prettier using a guard clause:

if current_user.manages?(current_organization) return

flash[:error] = t("organization_alliances.not_authorized")
redirect_to root_pat

end

def alliance_params
params.require(:organization_alliance).permit(:target_organization_id)
end
end
23 changes: 23 additions & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Organization < ApplicationRecord
has_many :inquiries
has_many :documents, as: :documentable, dependent: :destroy
has_many :petitions, dependent: :delete_all
has_many :source_alliances, class_name: "OrganizationAlliance", foreign_key: "source_organization_id", dependent: :destroy
has_many :target_alliances, class_name: "OrganizationAlliance", foreign_key: "target_organization_id", dependent: :destroy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this naming is clear enough, maybe:

initiated_alliances / received_alliances
outgoing_alliances / incoming_alliances

what do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I´m agree with you, I have changed source_alliances by source_alliances and target_alliances by received_alliances.

Thank you very much for your advice @franpb14 :)


validates :name, presence: true, uniqueness: true

Expand Down Expand Up @@ -61,6 +63,27 @@ def display_id
account.accountable_id
end

def alliance_with(organization)
source_alliances.find_by(target_organization: organization) ||
target_alliances.find_by(source_organization: organization)
end

def pending_sent_alliances
source_alliances.pending
end

def pending_received_alliances
target_alliances.pending
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not combine this with an or like the other 2?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented your suggestion to combine the methods for pending alliances using the or operator, just like we do with accepted and rejected alliances.

To make this change, I not only modified the Organization model by creating the pending_alliances method, but I also updated the controller organization_alliances_controller to use this new method:

# Before:
current_organization.pending_sent_alliances.includes(:source_organization, :target_organization) +
current_organization.pending_received_alliances.includes(:source_organization, :target_organization)

# After:
current_organization.pending_alliances.includes(:source_organization, :target_organization)


def accepted_alliances
source_alliances.accepted.or(target_alliances.accepted)
end

def rejected_alliances
source_alliances.rejected.or(target_alliances.rejected)
end

def ensure_reg_number_seq!
update_column(:reg_number_seq, members.maximum(:member_uid))
end
Expand Down
23 changes: 23 additions & 0 deletions app/models/organization_alliance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class OrganizationAlliance < ApplicationRecord
belongs_to :source_organization, class_name: "Organization"
belongs_to :target_organization, class_name: "Organization"

enum status: { pending: 0, accepted: 1, rejected: 2 }

validates :source_organization_id, presence: true
validates :target_organization_id, presence: true
validates :target_organization_id, uniqueness: { scope: :source_organization_id }
validate :cannot_ally_with_self

scope :pending, -> { where(status: "pending") }
scope :accepted, -> { where(status: "accepted") }
scope :rejected, -> { where(status: "rejected") }

private

def cannot_ally_with_self
if source_organization_id == target_organization_id
errors.add(:base, "Cannot create an alliance with yourself")
end
end
end
11 changes: 11 additions & 0 deletions app/policies/organization_alliance_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class OrganizationAlliancePolicy < ApplicationPolicy
def update?
alliance = record
user.manages?(alliance.source_organization) || user.manages?(alliance.target_organization)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to understand, why do we need this? and also this repeated code could be an auxiliar function

end

def destroy?
alliance = record
user.manages?(alliance.source_organization) || user.manages?(alliance.target_organization)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
<%= t('petitions.applications') %>
<% end %>
</li>
<li>
<%= link_to organization_alliances_path, class: "dropdown-item" do %>
<%= glyph :globe %>
<%= t "application.navbar.organization_alliances" %>
<% end %>
</li>
<li>
<%= link_to offers_path(org: current_organization), class: "dropdown-item" do %>
<%= glyph :link %>
Expand Down
102 changes: 102 additions & 0 deletions app/views/organization_alliances/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<h1><%= t('organization_alliances.title') %></h1>

<div class="row">
<div class="col-12 col-sm-12 col-md-12 col-lg-12">
Comment on lines +3 to +4
Copy link
Collaborator

@franpb14 franpb14 Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

col-12 is enough, since you are not giving other sizes

<ul class="nav nav-pills actions-menu">
<li class="nav-item">
<%= link_to organization_alliances_path(status: 'pending'), class: "nav-link #{'active' if @status == 'pending'}" do %>
<%= glyph :time %>
<%= t('organization_alliances.status.pending') %>
<% end %>
</li>
<li class="nav-item">
<%= link_to organization_alliances_path(status: 'accepted'), class: "nav-link #{'active' if @status == 'accepted'}" do %>
<%= glyph :ok %>
<%= t('organization_alliances.status.accepted') %>
<% end %>
</li>
<li class="nav-item">
<%= link_to organization_alliances_path(status: 'rejected'), class: "nav-link #{'active' if @status == 'rejected'}" do %>
<%= glyph :remove %>
<%= t('organization_alliances.status.rejected') %>
<% end %>
</li>
<li class="nav-item ms-auto">
<%= link_to organizations_path, class: "text-primary" do %>
<%= glyph :search %>
<%= t('organization_alliances.search_organizations') %>
<% end %>
</li>
</ul>
</div>
</div>

<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body table-responsive">
<table class="table table-hover table-sm">
<thead>
<tr>
<th><%= t('organization_alliances.organization') %></th>
<th><%= t('organization_alliances.city') %></th>
<th><%= t('organization_alliances.members') %></th>
<th><%= t('organization_alliances.type') %></th>
<% if @status != 'rejected' %>
<th><%= t('organization_alliances.actions') %></th>
<% end %>
</tr>
</thead>
<tbody>
<% @alliances.each do |alliance| %>
<% is_sender = (alliance.source_organization_id == current_organization.id) %>
<% other_org = is_sender ? alliance.target_organization : alliance.source_organization %>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this code to a helper, the views should have the less ruby code as possible

<tr>
<td><%= link_to other_org.name, other_org %></td>
<td><%= other_org.city %></td>
<td><%= other_org.members.count %></td>
<td>
<% if is_sender %>
<%= t('organization_alliances.sent') %>
<% else %>
<%= t('organization_alliances.received') %>
<% end %>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be just this:
<%= t("organization_alliances.#{is_sender ? 'sent' : 'received'}'") %>

</td>
<% if @status == 'pending' %>
<td>
<% if is_sender %>
<%= link_to t('organization_alliances.cancel_request'),
organization_alliance_path(alliance),
method: :delete,
class: 'btn btn-danger',
data: { confirm: t('organization_alliances.confirm_cancel') } %>
<% else %>
<div>
<%= link_to t('organization_alliances.accept'),
organization_alliance_path(alliance, status: 'accepted'),
method: :put,
class: 'btn btn-primary' %>
<%= link_to t('organization_alliances.reject'),
organization_alliance_path(alliance, status: 'rejected'),
method: :put,
class: 'btn btn-danger' %>
</div>
<% end %>
</td>
<% elsif @status == 'accepted' %>
<td>
<%= link_to t('organization_alliances.end_alliance'),
organization_alliance_path(alliance),
method: :delete,
class: 'btn btn-danger',
data: { confirm: t('organization_alliances.confirm_end') } %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
16 changes: 16 additions & 0 deletions app/views/organizations/_alliance_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<% if current_user&.manages?(current_organization) && organization != current_organization %>
<% alliance = current_organization.alliance_with(organization) %>
<% if alliance.nil? %>
<%= link_to t('organization_alliances.request_alliance'),
organization_alliances_path(organization_alliance: { target_organization_id: organization.id }),
method: :post,
class: 'btn btn-secondary',
aria: { label: t('organization_alliances.request_alliance_for', org: organization.name) } %>
<% elsif alliance.pending? %>
<span class="badge rounded-pill bg-secondary"><%= t('organization_alliances.pending') %></span>
<% elsif alliance.accepted? %>
<span class="badge rounded-pill bg-primary"><%= t('organization_alliances.active') %></span>
<% elsif alliance.rejected? %>
<span class="badge rounded-pill bg-danger"><%= t('organization_alliances.rejected') %></span>
<% end %>
<% end %>
7 changes: 6 additions & 1 deletion app/views/organizations/_organizations_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@
<td>
<%= render "organizations/petition_button", organization: org %>
</td>
</tr>
<% if current_user&.manages?(current_organization) %>
<td>
<%= render "organizations/alliance_button", organization: org %>
</td>
<% end %>
</tr>
7 changes: 5 additions & 2 deletions app/views/organizations/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
<th><%= t '.neighborhood' %></th>
<th><%= t '.web' %></th>
<th><%= t '.member_count' %></th>
<th></th>
<th><%= t '.membership' %></th>
<% if current_user&.manages?(current_organization) %>
<th><%= t '.alliance' %></th>
<% end %>
</tr>
</thead>
<tbody>
Expand All @@ -37,4 +40,4 @@
<%= paginate @organizations %>
</div>
</div>
</div>
</div>
34 changes: 33 additions & 1 deletion config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ en:
last_login: Last login
offer_public_link: Offers public link
organizations: Organizations
organization_alliances: Organizations
reports: Reports
sign_out: Logout
statistics: Statistics
Expand Down Expand Up @@ -370,6 +371,35 @@ en:
show:
give_time_for: Time transfer for this offer
offered_by: Offered by
organization_alliances:
title: "Organization Alliances"
created: "Alliance request sent"
updated: "Alliance status updated"
destroyed: "Alliance has been ended"
error_destroying: "Could not end alliance"
not_authorized: "You are not authorized to manage alliances"
organization: "Organization"
city: "City"
members: "Members"
type: "Type"
actions: "Actions"
sent: "Sent"
received: "Received"
pending: "Pending"
active: "Active"
rejected: "Rejected"
request_alliance: "Request alliance"
cancel_request: "Cancel request"
accept: "Accept"
reject: "Reject"
end_alliance: "End alliance"
confirm_cancel: "Are you sure you want to cancel this alliance request?"
confirm_end: "Are you sure you want to end this alliance?"
search_organizations: "Search organizations"
status:
pending: "Pending Requests"
accepted: "Active Alliances"
rejected: "Rejected Requests"
organization_notifier:
member_deleted:
body: User %{username} has unsubscribed from the organization.
Expand All @@ -382,6 +412,8 @@ en:
give_time: Give time to
index:
member_count: Number of users
membership: "Membership"
alliance: "Alliance"
new:
new: New bank
show:
Expand Down Expand Up @@ -588,4 +620,4 @@ en:
last: Last
next: Next
previous: Previous
truncate: Truncate
truncate: Truncate
Loading
Loading