diff --git a/app/assets/javascripts/application/organizations_filter.js b/app/assets/javascripts/application/organizations_filter.js new file mode 100644 index 00000000..0ae1c372 --- /dev/null +++ b/app/assets/javascripts/application/organizations_filter.js @@ -0,0 +1,28 @@ +$(function() { + $(document).on('change', '.organization-checkbox', function() { + var searchParams = new URLSearchParams(window.location.search); + var cat = searchParams.get('cat'); + var q = searchParams.get('q'); + var tag = searchParams.get('tag'); + + var form = $(this).closest('form'); + + if (cat) { + if (form.find('input[name="cat"]').length === 0) { + form.append(''); + } + } + + if (q) { + form.find('input[name="q"]').val(q); + } + + if (tag) { + if (form.find('input[name="tag"]').length === 0) { + form.append(''); + } + } + + form.submit(); + }); + }); diff --git a/app/controllers/organization_alliances_controller.rb b/app/controllers/organization_alliances_controller.rb index 27c340c2..2078158f 100644 --- a/app/controllers/organization_alliances_controller.rb +++ b/app/controllers/organization_alliances_controller.rb @@ -9,8 +9,7 @@ def index @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) + current_organization.pending_alliances.includes(:source_organization, :target_organization) when "accepted" current_organization.accepted_alliances.includes(:source_organization, :target_organization) when "rejected" @@ -21,24 +20,22 @@ def index end def create - @alliance = OrganizationAlliance.new( + alliance = OrganizationAlliance.new( source_organization: current_organization, - target_organization_id: params[:organization_alliance][:target_organization_id], + target_organization_id: alliance_params[:target_organization_id], status: "pending" ) - if @alliance.save + if alliance.save flash[:notice] = t("organization_alliances.created") else - flash[:error] = @alliance.errors.full_messages.to_sentence + flash[:error] = alliance.errors.full_messages.to_sentence end redirect_back fallback_location: organizations_path end def update - authorize @alliance - if @alliance.update(status: params[:status]) flash[:notice] = t("organization_alliances.updated") else @@ -49,8 +46,6 @@ def update end def destroy - authorize @alliance - if @alliance.destroy flash[:notice] = t("organization_alliances.destroyed") else @@ -67,10 +62,10 @@ def find_alliance end def authorize_admin - unless current_user.manages?(current_organization) - flash[:error] = t("organization_alliances.not_authorized") - redirect_to root_path - end + return if current_user.manages?(current_organization) + + flash[:error] = t("organization_alliances.not_authorized") + redirect_to root_path end def alliance_params diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 5a8e3616..23ff7ecc 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -6,10 +6,14 @@ class PostsController < ApplicationController def index context = model.active.of_active_members - if current_organization.present? - context = context.where( - organization_id: current_organization.id - ) + if current_user.present? && current_organization.present? + if params[:show_allied].present? + allied_org_ids = current_organization.allied_organizations.pluck(:id) + org_ids = [current_organization.id] + allied_org_ids + context = context.by_organizations(org_ids) + elsif !params[:org].present? + context = context.by_organization(current_organization.id) + end end posts = apply_scopes(context) @@ -98,15 +102,6 @@ def post_params end end - # TODO: remove this horrible hack ASAP - # - # This hack set the current organization to the post's - # organization, both in session and controller instance variable. - # - # Before changing the current organization it's important to check that - # the current_user is an active member of the organization. - # - # @param organization [Organization] def update_current_organization!(organization) return unless current_user && current_user.active?(organization) diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb new file mode 100644 index 00000000..59b8decd --- /dev/null +++ b/app/helpers/organizations_helper.rb @@ -0,0 +1,25 @@ +module OrganizationsHelper + def filterable_organizations + Organization.all.order(:name) + end + + def allied_organizations + return [] unless current_organization + + allied_org_ids = current_organization.accepted_alliances.map do |alliance| + alliance.source_organization_id == current_organization.id ? + alliance.target_organization_id : alliance.source_organization_id + end + + organizations = Organization.where(id: allied_org_ids + [current_organization.id]) + organizations.order(:name) + end + + def alliance_initiator?(alliance) + alliance.source_organization_id == current_organization.id + end + + def alliance_recipient(alliance) + alliance_initiator?(alliance) ? alliance.target_organization : alliance.source_organization + end +end diff --git a/app/models/organization.rb b/app/models/organization.rb index e2cc5dff..56280e84 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -24,8 +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 + has_many :initiated_alliances, class_name: "OrganizationAlliance", foreign_key: "source_organization_id", dependent: :destroy + has_many :received_alliances, class_name: "OrganizationAlliance", foreign_key: "target_organization_id", dependent: :destroy validates :name, presence: true, uniqueness: true @@ -54,34 +54,31 @@ def display_name_with_uid self end - # Returns the id to be displayed in the :new transfer page with the given - # destination_accountable - # - # @params destination_accountable [Organization | Object] target of a transfer - # @return [Integer | String] 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) + initiated_alliances.find_by(target_organization: organization) || + received_alliances.find_by(source_organization: organization) end - def pending_sent_alliances - source_alliances.pending - end - - def pending_received_alliances - target_alliances.pending + def pending_alliances + initiated_alliances.pending.or(received_alliances.pending) end def accepted_alliances - source_alliances.accepted.or(target_alliances.accepted) + initiated_alliances.accepted.or(received_alliances.accepted) end def rejected_alliances - source_alliances.rejected.or(target_alliances.rejected) + initiated_alliances.rejected.or(received_alliances.rejected) + end + + def allied_organizations + source_org_ids = initiated_alliances.accepted.pluck(:target_organization_id) + target_org_ids = received_alliances.accepted.pluck(:source_organization_id) + Organization.where(id: source_org_ids + target_org_ids) end def ensure_reg_number_seq! diff --git a/app/models/post.rb b/app/models/post.rb index 199755bf..bf586750 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -30,6 +30,9 @@ class Post < ApplicationRecord scope :by_organization, ->(org) { where(organization_id: org) if org } + scope :by_organizations, ->(org_ids) { + where(organization_id: org_ids) if org_ids.present? + } scope :of_active_members, -> { with_member.where("members.active") } diff --git a/app/policies/organization_alliance_policy.rb b/app/policies/organization_alliance_policy.rb deleted file mode 100644 index 53c3af04..00000000 --- a/app/policies/organization_alliance_policy.rb +++ /dev/null @@ -1,11 +0,0 @@ -class OrganizationAlliancePolicy < ApplicationPolicy - def update? - alliance = record - user.manages?(alliance.source_organization) || user.manages?(alliance.target_organization) - end - - def destroy? - alliance = record - user.manages?(alliance.source_organization) || user.manages?(alliance.target_organization) - end -end diff --git a/app/views/inquiries/index.html.erb b/app/views/inquiries/index.html.erb index 198dbc77..a72f20fc 100644 --- a/app/views/inquiries/index.html.erb +++ b/app/views/inquiries/index.html.erb @@ -11,7 +11,7 @@ <%= render "shared/post_filters", base_path: inquiries_path %>
- <% if current_user && current_organization && !params[:org] %> + <% if current_user && current_organization %>
- \ No newline at end of file + diff --git a/app/views/organizations/_alliance_button.html.erb b/app/views/organizations/_alliance_button.html.erb index fc7f5d09..6be3d44e 100644 --- a/app/views/organizations/_alliance_button.html.erb +++ b/app/views/organizations/_alliance_button.html.erb @@ -13,4 +13,4 @@ <% elsif alliance.rejected? %> <%= t('organization_alliances.rejected') %> <% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/organizations/_organizations_row.html.erb b/app/views/organizations/_organizations_row.html.erb index 1a834b24..30b5a6c5 100644 --- a/app/views/organizations/_organizations_row.html.erb +++ b/app/views/organizations/_organizations_row.html.erb @@ -12,4 +12,4 @@ <%= render "organizations/alliance_button", organization: org %> <% end %> - \ No newline at end of file + diff --git a/app/views/organizations/index.html.erb b/app/views/organizations/index.html.erb index 3860b986..f750148d 100644 --- a/app/views/organizations/index.html.erb +++ b/app/views/organizations/index.html.erb @@ -40,4 +40,4 @@ <%= paginate @organizations %> - \ No newline at end of file + diff --git a/app/views/shared/_post_filters.html.erb b/app/views/shared/_post_filters.html.erb index d471cb0c..e15432af 100644 --- a/app/views/shared/_post_filters.html.erb +++ b/app/views/shared/_post_filters.html.erb @@ -1,4 +1,5 @@ <% @category = Category.find_by(id: params[:cat]) %> +<% selected_org = Organization.find_by(id: params[:org]) %>
+
-
+ \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 1d6cf127..a095709e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -101,8 +101,8 @@ en: one: Offer other: Offers organization: - one: Time Bank - other: Time Banks + one: Organization + other: Organizations post: one: Post other: Posts @@ -304,6 +304,7 @@ en: table: actions: Actions to: To + filter_by_organizations: "Filter by organizations" inquiries: edit: submit: Change request @@ -620,4 +621,4 @@ en: last: Last next: Next previous: Previous - truncate: Truncate \ No newline at end of file + truncate: Truncate diff --git a/config/locales/es.yml b/config/locales/es.yml index 4bad6558..7e8f3492 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -101,8 +101,8 @@ es: one: Oferta other: Ofertas organization: - one: Banco de Tiempo - other: Bancos de Tiempo + one: Organización + other: Organizaciones post: one: Anuncio other: Anuncios @@ -304,6 +304,7 @@ es: table: actions: Acciones to: a + filter_by_organizations: "Filtrar por organizaciones" inquiries: edit: submit: Cambiar demanda @@ -385,7 +386,7 @@ es: actions: "Acciones" sent: "Enviadas" received: "Recibidas" - pending: "Pending" + pending: "Pendientes" active: "Activa" rejected: "Rechazada" request_alliance: "Solicitar alianza" @@ -620,4 +621,4 @@ es: last: Ultima » next: Siguiente › previous: "‹ Anterior" - truncate: "…" \ No newline at end of file + truncate: "…" diff --git a/db/structure.sql b/db/structure.sql index 21304444..4388a527 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,13 +1,25 @@ SET statement_timeout = 0; SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); SET check_function_bodies = false; SET xmloption = content; SET client_min_messages = warning; -SET row_security = off; + +-- +-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; + + +-- +-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; + -- -- Name: hstore; Type: EXTENSION; Schema: -; Owner: - @@ -70,7 +82,7 @@ CREATE FUNCTION public.posts_trigger() RETURNS trigger SET default_tablespace = ''; -SET default_table_access_method = heap; +SET default_with_oids = false; -- -- Name: accounts; Type: TABLE; Schema: public; Owner: - @@ -1289,7 +1301,7 @@ CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING b -- Name: posts tsvectorupdate; Type: TRIGGER; Schema: public; Owner: - -- -CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON public.posts FOR EACH ROW EXECUTE FUNCTION public.posts_trigger(); +CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON public.posts FOR EACH ROW EXECUTE PROCEDURE public.posts_trigger(); -- @@ -1395,79 +1407,78 @@ ALTER TABLE ONLY public.organization_alliances SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES -('20250412110249'), -('20250215163406'), -('20250215163405'), -('20250215163404'), -('20241230170753'), -('20231120164346'), -('20231120164231'), -('20230401114456'), -('20230314233504'), -('20230312231058'), -('20221016192111'), -('20210503201944'), -('20210502160343'), -('20210424174640'), -('20210423193937'), -('20190523225323'), -('20190523213421'), -('20190412163011'), -('20190411192828'), -('20190322180602'), -('20190319121401'), -('20181004200104'), -('20180924164456'), -('20180831161349'), -('20180828160700'), -('20180604145622'), -('20180530180546'), -('20180529144243'), -('20180525141138'), -('20180524143938'), -('20180514193153'), -('20180501093846'), -('20180221161343'), -('20150422162806'), -('20150330200315'), -('20150329193421'), -('20140514225527'), -('20140513141718'), -('20140119161433'), -('20131231110424'), -('20131227155440'), -('20131227142805'), -('20131227110122'), -('20131220160257'), -('20131104032622'), -('20131104013829'), -('20131104013634'), -('20131104004235'), -('20131103221044'), -('20131029202724'), -('20131027215517'), -('20131025202608'), -('20131017144321'), -('20130723160206'), -('20130703234042'), -('20130703234011'), -('20130703233851'), -('20130621105452'), -('20130621103501'), -('20130621103053'), -('20130621102219'), -('20130618210236'), -('20130514094755'), -('20130513092219'), -('20130508085004'), -('20130425165150'), -('20130222185624'), -('20130214181128'), -('20130214175758'), -('20121121233818'), -('20121104085711'), -('20121104004639'), -('20121019101022'), +('1'), ('2'), -('1'); - +('20121019101022'), +('20121104004639'), +('20121104085711'), +('20121121233818'), +('20130214175758'), +('20130214181128'), +('20130222185624'), +('20130425165150'), +('20130508085004'), +('20130513092219'), +('20130514094755'), +('20130618210236'), +('20130621102219'), +('20130621103053'), +('20130621103501'), +('20130621105452'), +('20130703233851'), +('20130703234011'), +('20130703234042'), +('20130723160206'), +('20131017144321'), +('20131025202608'), +('20131027215517'), +('20131029202724'), +('20131103221044'), +('20131104004235'), +('20131104013634'), +('20131104013829'), +('20131104032622'), +('20131220160257'), +('20131227110122'), +('20131227142805'), +('20131227155440'), +('20131231110424'), +('20140119161433'), +('20140513141718'), +('20140514225527'), +('20150329193421'), +('20150330200315'), +('20150422162806'), +('20180221161343'), +('20180501093846'), +('20180514193153'), +('20180524143938'), +('20180525141138'), +('20180529144243'), +('20180530180546'), +('20180604145622'), +('20180828160700'), +('20180831161349'), +('20180924164456'), +('20181004200104'), +('20190319121401'), +('20190322180602'), +('20190411192828'), +('20190412163011'), +('20190523213421'), +('20190523225323'), +('20210423193937'), +('20210424174640'), +('20210502160343'), +('20210503201944'), +('20221016192111'), +('20230312231058'), +('20230314233504'), +('20230401114456'), +('20231120164231'), +('20231120164346'), +('20241230170753'), +('20250215163404'), +('20250215163405'), +('20250215163406'), +('20250412110249'); diff --git a/spec/controllers/offers_controller_spec.rb b/spec/controllers/offers_controller_spec.rb index 82a1adc9..f47bf4f7 100644 --- a/spec/controllers/offers_controller_spec.rb +++ b/spec/controllers/offers_controller_spec.rb @@ -52,15 +52,48 @@ expect(assigns(:offers)).to eq([other_offer]) end end + + context "when filtering by organization" do + let(:organization1) { Fabricate(:organization) } + let(:organization2) { Fabricate(:organization) } + let(:user1) { Fabricate(:user) } + let(:user2) { Fabricate(:user) } + let(:member1) { Fabricate(:member, user: user1, organization: organization1) } + let(:member2) { Fabricate(:member, user: user2, organization: organization2) } + let!(:offer1) { Fabricate(:offer, user: user1, organization: organization1, title: "Ruby on Rails nivel principiante") } + let!(:offer2) { Fabricate(:offer, user: user2, organization: organization2, title: "Cocina low cost") } + + before do + member1 + member2 + login(user1) + Fabricate(:member, user: user1, organization: organization2) unless user1.members.where(organization: organization2).exists? + end + + it 'displays only offers from the selected organization' do + get :index, params: { org: organization1.id } + expect(assigns(:offers)).to include(offer1) + expect(assigns(:offers)).not_to include(offer2) + end + + it 'displays all offers when no organization is selected' do + get :index + expect(assigns(:offers)).to include(offer1) + expect(assigns(:offers)).to include(offer2) + end + end end context "with another organization" do it "skips the original org's offers" do - login(yet_another_member.user) + separate_organization = Fabricate(:organization) + separate_user = Fabricate(:user) - get :index + login(separate_user) + + get :index, params: { org: separate_organization.id } - expect(assigns(:offers)).to eq([]) + expect(assigns(:offers).map(&:organization_id).uniq).to eq([separate_organization.id]) unless assigns(:offers).empty? end end end diff --git a/spec/features/Offers_organization_filtering_spec.rb b/spec/features/Offers_organization_filtering_spec.rb new file mode 100644 index 00000000..fe7ba919 --- /dev/null +++ b/spec/features/Offers_organization_filtering_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +RSpec.feature 'Offers organization filtering' do + let(:organization) { Fabricate(:organization) } + let(:other_organization) { Fabricate(:organization) } + let(:category) { Fabricate(:category) } + + let(:user) do + u = Fabricate(:user, password: "12345test", password_confirmation: "12345test") + u.terms_accepted_at = Time.current + u.save! + u + end + + let!(:member) { Fabricate(:member, organization: organization, user: user) } + let!(:other_member) { Fabricate(:member, organization: other_organization) } + + before do + OrganizationAlliance.create!( + source_organization: organization, + target_organization: other_organization, + status: "accepted" + ) + + Fabricate(:offer, + user: user, + organization: organization, + category: category, + title: "Local offer", + active: true) + + Fabricate(:offer, + user: other_member.user, + organization: other_organization, + category: category, + title: "Allied offer", + active: true) + + sign_in_with(user.email, "12345test") + end + + scenario 'User filters posts by allied organization' do + visit offers_path + + expect(page).to have_content("Local offer") + expect(page).to have_content("Allied offer") + + find('a.dropdown-toggle', text: Organization.model_name.human(count: :other)).click + + query_params = { org: other_organization.id } + link_path = "#{offers_path}?#{query_params.to_query}" + visit link_path + + expect(page).to have_content("Allied offer") + expect(page).not_to have_content("Local offer") + end +end diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 868c5af3..dfbbd0c3 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -136,14 +136,11 @@ ) end - it "returns pending sent alliances" do - expect(organization.pending_sent_alliances).to include(@pending_sent) - expect(organization.pending_sent_alliances).not_to include(@pending_received) - end - - it "returns pending received alliances" do - expect(organization.pending_received_alliances).to include(@pending_received) - expect(organization.pending_received_alliances).not_to include(@pending_sent) + it "returns pending alliances" do + expect(organization.pending_alliances).to include(@pending_sent, @pending_received) + expect(organization.pending_alliances).not_to include( + @accepted_sent, @accepted_received, @rejected_sent, @rejected_received + ) end it "returns accepted alliances" do diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 55ba751c..035e7813 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -6,4 +6,29 @@ it { is_expected.to have_many(:movements) } it { is_expected.to have_many(:events) } end + + describe '.by_organizations' do + let(:organization) { Fabricate(:organization) } + let(:other_organization) { Fabricate(:organization) } + let(:member) { Fabricate(:member, organization: organization) } + let(:other_member) { Fabricate(:member, organization: other_organization) } + let(:category) { Fabricate(:category) } + let!(:post1) { Fabricate(:offer, user: member.user, organization: organization, category: category) } + let!(:post2) { Fabricate(:offer, user: other_member.user, organization: other_organization, category: category) } + + it 'returns posts from the specified organizations' do + expect(Post.by_organizations([organization.id])).to include(post1) + expect(Post.by_organizations([organization.id])).not_to include(post2) + + expect(Post.by_organizations([other_organization.id])).to include(post2) + expect(Post.by_organizations([other_organization.id])).not_to include(post1) + + expect(Post.by_organizations([organization.id, other_organization.id])).to include(post1, post2) + end + + it 'returns all posts if no organization ids are provided' do + expect(Post.by_organizations(nil)).to include(post1, post2) + expect(Post.by_organizations([])).to include(post1, post2) + end + end end