From 7e69876efeef0b7e3d3b5ef3711f84331c43ac81 Mon Sep 17 00:00:00 2001 From: gmartincor Date: Tue, 15 Apr 2025 19:53:48 +0200 Subject: [PATCH 1/9] Filter offer by organizations --- .../application/organizations_filter.js | 33 ++++++++++++++++ app/controllers/posts_controller.rb | 6 --- app/helpers/organizations_helper.rb | 17 ++++++++ app/models/post.rb | 3 ++ app/views/inquiries/index.html.erb | 2 +- app/views/offers/index.html.erb | 2 +- app/views/shared/_post_filters.html.erb | 39 +++++++++++++++++-- config/locales/en.yml | 1 + config/locales/es.yml | 5 ++- 9 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/application/organizations_filter.js create mode 100644 app/helpers/organizations_helper.rb diff --git a/app/assets/javascripts/application/organizations_filter.js b/app/assets/javascripts/application/organizations_filter.js new file mode 100644 index 000000000..964149118 --- /dev/null +++ b/app/assets/javascripts/application/organizations_filter.js @@ -0,0 +1,33 @@ +// app/assets/javascripts/application/organization_filter.js +$(function() { + // Manejar cambios en las casillas de organizaciones + $(document).on('change', '.organization-checkbox', function() { + // Obtener valores actuales de la URL + 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'); + + // Mantener parámetros actuales + 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(''); + } + } + + // Enviar el formulario + form.submit(); + }); + }); \ No newline at end of file diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 5a8e3616b..4ef33dea4 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -6,12 +6,6 @@ class PostsController < ApplicationController def index context = model.active.of_active_members - if current_organization.present? - context = context.where( - organization_id: current_organization.id - ) - end - posts = apply_scopes(context) posts = posts.search_by_query(params[:q]) if params[:q].present? posts = posts.page(params[:page]).per(25) diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb new file mode 100644 index 000000000..47438672b --- /dev/null +++ b/app/helpers/organizations_helper.rb @@ -0,0 +1,17 @@ +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 +end diff --git a/app/models/post.rb b/app/models/post.rb index 199755bf3..bf5867500 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/views/inquiries/index.html.erb b/app/views/inquiries/index.html.erb index 198dbc77d..a72f20fc8 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/config/locales/en.yml b/config/locales/en.yml index 1d6cf1274..cff6319f1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -304,6 +304,7 @@ en: table: actions: Actions to: To + filter_by_organizations: "Filter by organizations" inquiries: edit: submit: Change request diff --git a/config/locales/es.yml b/config/locales/es.yml index 4bad65582..1d0992ae0 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 From 40b6fc42329771701b8b3ac910d382c925cb92ca Mon Sep 17 00:00:00 2001 From: gmartincor Date: Tue, 15 Apr 2025 19:56:12 +0200 Subject: [PATCH 2/9] Change name --- config/locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index cff6319f1..523385ebb 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 From 9249048ff903b55b67221e664bb2bf6ed0f6721f Mon Sep 17 00:00:00 2001 From: gmartincor Date: Wed, 16 Apr 2025 12:43:25 +0200 Subject: [PATCH 3/9] new tests offers visibility --- spec/controllers/offers_controller_spec.rb | 26 ++++++++ .../Offers_organization_filtering_spec.rb | 60 +++++++++++++++++++ spec/models/post_spec.rb | 25 ++++++++ 3 files changed, 111 insertions(+) create mode 100644 spec/features/Offers_organization_filtering_spec.rb diff --git a/spec/controllers/offers_controller_spec.rb b/spec/controllers/offers_controller_spec.rb index 82a1adc9c..ca832adb1 100644 --- a/spec/controllers/offers_controller_spec.rb +++ b/spec/controllers/offers_controller_spec.rb @@ -52,6 +52,32 @@ expect(assigns(:offers)).to eq([other_offer]) end end + + context "when filtering by organization" do + let(:organization1) { Organization.find_by(name: "Banco de Tiempo Local") } + let(:organization2) { Organization.find_by(name: "El otro Banco de Tiempo :)") } + let(:user1) { User.find_by(email: "user@timeoverflow.org") } + let(:user2) { User.find_by(email: "user2@timeoverflow.org") } + let!(:offer1) { Offer.find_by(title: "Ruby on Rails nivel principiante") || + Fabricate(:offer, user: user1, title: "Ruby on Rails nivel principiante") } + let!(:offer2) { Offer.find_by(title: "Cocina low cost") || + Fabricate(:offer, user: user2, title: "Cocina low cost") } + + before { login(user1) } + + it 'displays only offers from the selected organization' do + get :index, params: { organization_id: 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, offer2) + end + end end context "with another organization" do diff --git a/spec/features/Offers_organization_filtering_spec.rb b/spec/features/Offers_organization_filtering_spec.rb new file mode 100644 index 000000000..d909989fe --- /dev/null +++ b/spec/features/Offers_organization_filtering_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +RSpec.feature 'Offers organization filtering' do + let(:organization) { Fabricate(:organization) } + let(:other_organization) { Fabricate(:organization) } + let(:category) { Fabricate(:category) } + let(:member) { Fabricate(:member, organization: organization) } + let(:other_member) { Fabricate(:member, organization: other_organization) } + let(:user) { member.user } + + before do + user.terms_accepted_at = Time.current + user.save! + + # Create an accepted alliance + OrganizationAlliance.create!( + source_organization: organization, + target_organization: other_organization, + status: "accepted" + ) + + # Create posts in both organizations + 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) + + # Log in as user + sign_in_with(user.email, user.password) + end + + scenario 'User filters posts by allied organization' do + visit offers_path + + # Should see posts from both organizations by default + expect(page).to have_content("Local offer") + expect(page).to have_content("Allied offer") + + # Click on the organization dropdown toggle + find('a.dropdown-toggle', text: Organization.model_name.human(count: :other)).click + + # Find the organization in the dropdown menu and click it directly by url + query_params = { org: other_organization.id } + link_path = "#{offers_path}?#{query_params.to_query}" + visit link_path + + # Should see only posts from selected organization + expect(page).to have_content("Allied offer") + expect(page).not_to have_content("Local offer") + end +end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 55ba751c4..035e7813b 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 From 8e1e9c58e1ed4a17c3fcdd6cf912574fbf07409b Mon Sep 17 00:00:00 2001 From: gmartincor Date: Wed, 16 Apr 2025 18:15:41 +0200 Subject: [PATCH 4/9] new test Offers Visibility --- spec/controllers/offers_controller_spec.rb | 40 +++++++++++-------- .../Offers_organization_filtering_spec.rb | 25 +++++------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/spec/controllers/offers_controller_spec.rb b/spec/controllers/offers_controller_spec.rb index ca832adb1..d685744bc 100644 --- a/spec/controllers/offers_controller_spec.rb +++ b/spec/controllers/offers_controller_spec.rb @@ -54,39 +54,47 @@ end context "when filtering by organization" do - let(:organization1) { Organization.find_by(name: "Banco de Tiempo Local") } - let(:organization2) { Organization.find_by(name: "El otro Banco de Tiempo :)") } - let(:user1) { User.find_by(email: "user@timeoverflow.org") } - let(:user2) { User.find_by(email: "user2@timeoverflow.org") } - let!(:offer1) { Offer.find_by(title: "Ruby on Rails nivel principiante") || - Fabricate(:offer, user: user1, title: "Ruby on Rails nivel principiante") } - let!(:offer2) { Offer.find_by(title: "Cocina low cost") || - Fabricate(:offer, user: user2, title: "Cocina low cost") } + 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 { login(user1) } + 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: { organization_id: organization1.id } - + 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, offer2) + 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) + separate_member = Fabricate(:member, organization: separate_organization, user: separate_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 index d909989fe..fe7ba9193 100644 --- a/spec/features/Offers_organization_filtering_spec.rb +++ b/spec/features/Offers_organization_filtering_spec.rb @@ -4,22 +4,24 @@ let(:organization) { Fabricate(:organization) } let(:other_organization) { Fabricate(:organization) } let(:category) { Fabricate(:category) } - let(:member) { Fabricate(:member, organization: organization) } - let(:other_member) { Fabricate(:member, organization: other_organization) } - let(:user) { member.user } - before do - user.terms_accepted_at = Time.current - user.save! + let(:user) do + u = Fabricate(:user, password: "12345test", password_confirmation: "12345test") + u.terms_accepted_at = Time.current + u.save! + u + end - # Create an accepted alliance + 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" ) - # Create posts in both organizations Fabricate(:offer, user: user, organization: organization, @@ -34,26 +36,21 @@ title: "Allied offer", active: true) - # Log in as user - sign_in_with(user.email, user.password) + sign_in_with(user.email, "12345test") end scenario 'User filters posts by allied organization' do visit offers_path - # Should see posts from both organizations by default expect(page).to have_content("Local offer") expect(page).to have_content("Allied offer") - # Click on the organization dropdown toggle find('a.dropdown-toggle', text: Organization.model_name.human(count: :other)).click - # Find the organization in the dropdown menu and click it directly by url query_params = { org: other_organization.id } link_path = "#{offers_path}?#{query_params.to_query}" visit link_path - # Should see only posts from selected organization expect(page).to have_content("Allied offer") expect(page).not_to have_content("Local offer") end From 08a27114c6ffa324280c9d8a2b7d00001f791768 Mon Sep 17 00:00:00 2001 From: gmartincor Date: Thu, 17 Apr 2025 08:59:09 +0200 Subject: [PATCH 5/9] Correc test offers_controller_spec.rb --- spec/controllers/offers_controller_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/controllers/offers_controller_spec.rb b/spec/controllers/offers_controller_spec.rb index d685744bc..f47bf4f76 100644 --- a/spec/controllers/offers_controller_spec.rb +++ b/spec/controllers/offers_controller_spec.rb @@ -88,7 +88,6 @@ it "skips the original org's offers" do separate_organization = Fabricate(:organization) separate_user = Fabricate(:user) - separate_member = Fabricate(:member, organization: separate_organization, user: separate_user) login(separate_user) From 66663daae82b223305990b112499333d0f9506d5 Mon Sep 17 00:00:00 2001 From: gmartincor Date: Sun, 20 Apr 2025 18:05:27 +0200 Subject: [PATCH 6/9] refactor code --- app/controllers/posts_controller.rb | 19 ++++++++++--------- app/models/organization.rb | 6 ++++++ app/views/shared/_post_filters.html.erb | 8 +++++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 4ef33dea4..23ff7ecc3 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -6,6 +6,16 @@ class PostsController < ApplicationController def index context = model.active.of_active_members + 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) posts = posts.search_by_query(params[:q]) if params[:q].present? posts = posts.page(params[:page]).per(25) @@ -92,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/models/organization.rb b/app/models/organization.rb index e2cc5dff7..2d15c9ee5 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -84,6 +84,12 @@ def rejected_alliances source_alliances.rejected.or(target_alliances.rejected) end + def allied_organizations + source_org_ids = source_alliances.accepted.pluck(:target_organization_id) + target_org_ids = target_alliances.accepted.pluck(:source_organization_id) + Organization.where(id: source_org_ids + target_org_ids) + end + def ensure_reg_number_seq! update_column(:reg_number_seq, members.maximum(:member_uid)) end diff --git a/app/views/shared/_post_filters.html.erb b/app/views/shared/_post_filters.html.erb index cf92f46d0..e15432af8 100644 --- a/app/views/shared/_post_filters.html.erb +++ b/app/views/shared/_post_filters.html.erb @@ -51,7 +51,13 @@