diff --git a/Gemfile b/Gemfile index 0317203bb..173ad6a3b 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ ruby File.read('.ruby-version').strip gem 'rails', '~> 6.1.1' gem 'rails-i18n', '~> 6.0.0' gem 'rdiscount', '~> 2.2.0.1' +gem 'rubyzip', '~> 2.3.0' gem 'activeadmin', '~> 2.9.0' gem 'bootsnap', '~> 1.7.3', require: false gem 'has_scope', '~> 0.7.2' diff --git a/Gemfile.lock b/Gemfile.lock index 62465c48e..d9eb8b504 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -442,6 +442,7 @@ DEPENDENCIES rspec-rails (~> 4.0.0) rubocop (~> 1.6) rubocop-rails (~> 2.9) + rubyzip (~> 2.3.0) sassc-rails (~> 2.1.2) select2-rails (~> 4.0.13) selenium-webdriver (~> 3.142) diff --git a/app/admin/post.rb b/app/admin/post.rb index 976788ef9..5aef7fe3c 100644 --- a/app/admin/post.rb +++ b/app/admin/post.rb @@ -2,6 +2,7 @@ index do id_column column :class + column :is_group column :title column :created_at do |post| l post.created_at.to_date, format: :long @@ -19,10 +20,11 @@ f.input :type, as: :radio, collection: %w[Offer Inquiry] f.input :title f.input :organization - f.input :user, hint: "* should be member of the selected organization" + f.input :user, hint: "Should be member of the selected organization" f.input :category f.input :description - f.input :tag_list + f.input :tag_list, hint: "Accepts comma separated values" + f.input :is_group f.input :active end f.actions @@ -36,6 +38,7 @@ filter :organization filter :user filter :category + filter :is_group filter :active filter :created_at end diff --git a/app/admin/user.rb b/app/admin/user.rb index 8d273e89e..7de9e123b 100644 --- a/app/admin/user.rb +++ b/app/admin/user.rb @@ -29,12 +29,14 @@ filter :organizations filter :email filter :username + filter :phone form do |f| f.semantic_errors *f.object.errors.keys f.inputs do f.input :username f.input :email + f.input :phone f.input :gender, as: :select, collection: User::GENDERS f.input :identity_document end diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 8df55ae59..b6047b93d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -221,7 +221,7 @@ html { } .row.exports { - padding: 10px; + padding: 20px 0; } .table-responsive { diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 0c747804a..42eae556c 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -2,7 +2,9 @@ class OrganizationsController < ApplicationController before_action :load_resource, only: [:show, :edit, :update, :set_current] def index - @organizations = Organization.all.page(params[:page]).per(25) + organizations = Organization.all + organizations = organizations.search_by_query(params[:q]) if params[:q].present? + @organizations = organizations.page(params[:page]).per(25) end def show diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index b757574e3..5b2b271fa 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -1,48 +1,98 @@ +require 'zip' + class ReportsController < ApplicationController before_action :authenticate_user! layout "report" def user_list - @members = current_organization.members.active. - includes(:user). - order("members.member_uid") + @members = report_collection("Member") report_responder('Member', current_organization, @members) end def post_list @post_type = (params[:type] || "offer").capitalize.constantize - @posts = current_organization.posts. - of_active_members. - active. - merge(@post_type.all). - includes(:user, :category). - group_by(&:category). - to_a. - sort_by { |category, _| category.try(:name).to_s } + @posts = report_collection(@post_type) report_responder('Post', current_organization, @posts, @post_type) end def transfer_list - @transfers = current_organization.all_transfers_with_accounts + @transfers = report_collection('Transfer') report_responder('Transfer', current_organization, @transfers) end + def download_all + filename = "#{current_organization.name.parameterize}_#{Date.today}.zip" + temp_file = Tempfile.new(filename) + + begin + Zip::File.open(temp_file.path, Zip::File::CREATE) do |zipfile| + %w(Member Transfer Inquiry Offer).each do |report_class| + add_csv_to_zip(report_class, zipfile) + end + end + zip_data = File.read(temp_file.path) + send_data(zip_data, type: 'application/zip', disposition: 'attachment', filename: filename) + rescue Errno::ENOENT + redirect_to download_all_report_path + ensure + temp_file.close + temp_file.unlink + end + end + private def report_responder(report_class, *args) respond_to do |format| format.html - format.csv { download_report("Report::Csv::#{report_class}", *args) } - format.pdf { download_report("Report::Pdf::#{report_class}", *args) } + format.csv { download_report("Csv::#{report_class}", *args) } + format.pdf { download_report("Pdf::#{report_class}", *args) } end end def download_report(report_class, *args) - report = report_class.constantize.new(*args) + report = get_report(report_class, *args) send_data report.run, filename: report.name, type: report.mime_type end + + def get_report(report_class, *args) + "Report::#{report_class}".constantize.new(*args) + end + + def report_collection(report_class) + case report_class.to_s + when 'Member' + current_organization.members.active.includes(:user).order('members.member_uid') + when 'Transfer' + current_organization.all_transfers_with_accounts + when 'Inquiry', 'Offer' + report_class = report_class.constantize if report_class.is_a?(String) + + current_organization.posts.of_active_members.active. + merge(report_class.all). + includes(:user, :category). + group_by(&:category). + sort_by { |category, _| category.try(:name).to_s } + end + end + + def add_csv_to_zip(report_class, zip) + collection = report_collection(report_class) + + report = if report_class.in? %w(Inquiry Offer) + get_report("Csv::Post", current_organization, collection, report_class.constantize) + else + get_report("Csv::#{report_class}", current_organization, collection) + end + + file = Tempfile.new + file.write(report.run) + file.rewind + + zip.add("#{report_class.pluralize}_#{Date.today}.csv", file.path) + end end diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb index 0a499bbf1..61e3d6f78 100644 --- a/app/models/concerns/taggable.rb +++ b/app/models/concerns/taggable.rb @@ -13,8 +13,10 @@ def tag_list tags && tags.join(", ") end - def tag_list=(tag_list) - self.tags = tag_list.reject(&:empty?) + def tag_list=(new_tags) + new_tags = new_tags.split(",").map(&:strip) if new_tags.is_a?(String) + + self.tags = new_tags.reject(&:empty?) end module ClassMethods diff --git a/app/models/organization.rb b/app/models/organization.rb index 1fc189017..dfc7a4b00 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,4 +1,15 @@ class Organization < ApplicationRecord + include PgSearch::Model + + pg_search_scope :search_by_query, + against: %i[city neighborhood address name], + ignoring: :accents, + using: { + tsearch: { + prefix: true + } + } + has_many :members, dependent: :destroy has_many :users, -> { order "members.created_at DESC" }, through: :members has_many :all_accounts, class_name: "Account", inverse_of: :organization, dependent: :destroy diff --git a/app/views/application/menus/_organization_reports_menu.html.erb b/app/views/application/menus/_organization_reports_menu.html.erb index d87571675..9b590189b 100644 --- a/app/views/application/menus/_organization_reports_menu.html.erb +++ b/app/views/application/menus/_organization_reports_menu.html.erb @@ -29,5 +29,12 @@ <%= Transfer.model_name.human(count: :many) %> <% end %> + +
  • + <%= link_to download_all_report_path do %> + <%= glyph :download %> + <%= t 'reports.download_all' %> + <% end %> +
  • diff --git a/app/views/application/menus/_visitor_menu.html.erb b/app/views/application/menus/_visitor_menu.html.erb index 513a63e63..ee04b5d1f 100644 --- a/app/views/application/menus/_visitor_menu.html.erb +++ b/app/views/application/menus/_visitor_menu.html.erb @@ -1,2 +1,3 @@
  • <%= link_to t("layouts.application.login"), new_user_session_path %>
  • <%= link_to t("layouts.application.about"), page_path("about") %>
  • +
  • <%= link_to t("layouts.application.bdtnear"), organizations_path %>
  • diff --git a/app/views/organizations/index.html.erb b/app/views/organizations/index.html.erb index 20090cac9..1f3ccad94 100644 --- a/app/views/organizations/index.html.erb +++ b/app/views/organizations/index.html.erb @@ -2,6 +2,21 @@ <%= Organization.model_name.human(count: :many) %> +
    +
    + +
    +
    +
    diff --git a/app/views/organizations/show.html.erb b/app/views/organizations/show.html.erb index d14dca13d..fb5eaee7c 100644 --- a/app/views/organizations/show.html.erb +++ b/app/views/organizations/show.html.erb @@ -106,4 +106,8 @@ <% if current_user %> <%= render "shared/movements" %> +<% else %> +
    + <%= t ".join_timebank" %> +
    <% end %> diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 6d7aa4838..6087868ca 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -293,6 +293,7 @@ ca: required_field: "* Camp obligatori" save: Desar search: Cercar + search_location: Cerca per localitat show: Mostrar source_destination: Origen/Destí statistics: Estadístiques @@ -309,6 +310,7 @@ ca: layouts: application: about: Sobre TimeOverflow + bdtnear: Cerca Banc de Temps edit_org: Modificar %{organization} edit_profile: Modificar perfil help: Ajuda @@ -377,6 +379,7 @@ ca: new: Nou banc show: contact_information: Informació de contacte + join_timebank: No dubtis en contactar amb el Banc de Temps per unir-te o per preguntar qualsevol dubte. pages: about: app-mobile: Aplicació Mòbil @@ -412,11 +415,8 @@ ca: show: info: Aquesta %{type} pertany a %{organization}. reports: - cat_with_users: - title: Serveis ofertats download: Descarregar - user_list: - title: Llistat d'usuaris + download_all: Descarregar tot shared: movements: delete_reason: Esteu segur d'esborrar aquest comentari? diff --git a/config/locales/en.yml b/config/locales/en.yml index e7419738b..2a65f5c3e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -293,6 +293,7 @@ en: required_field: "* Required field" save: Save search: Search + search_location: Search by location show: Show source_destination: From/to statistics: Statistics @@ -309,6 +310,7 @@ en: layouts: application: about: About TimeOverflow + bdtnear: Search Timebank edit_org: Update %{organization} edit_profile: Update my profile help: Help @@ -377,6 +379,7 @@ en: new: New bank show: contact_information: Contact information + join_timebank: Don't hesitate to contact the time bank to join it or ask any questions. pages: about: app-mobile: Mobile App @@ -412,11 +415,8 @@ en: show: info: This %{type} belongs to %{organization}. reports: - cat_with_users: - title: Offered Services download: Download - user_list: - title: User List + download_all: Download all shared: movements: delete_reason: Are you sure to delete this comment? diff --git a/config/locales/es.yml b/config/locales/es.yml index 617a1a733..91d7203b4 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -293,6 +293,7 @@ es: required_field: "* Campo obligatorio" save: Guardar search: Buscar + search_location: Buscar por localidad show: Mostrar source_destination: Origen/Destino statistics: Estadísticas @@ -309,6 +310,7 @@ es: layouts: application: about: Sobre TimeOverflow + bdtnear: Busca Banco de Tiempo edit_org: Modificar %{organization} edit_profile: Modificar mi perfil help: Ayuda @@ -377,6 +379,7 @@ es: new: Nuevo banco show: contact_information: Información de contacto + join_timebank: No dudes en contactar con el Banco de Tiempo para unirte o para resolver dudas. pages: about: app-mobile: App Móvil @@ -412,11 +415,8 @@ es: show: info: Esta %{type} pertenece a %{organization}. reports: - cat_with_users: - title: Servicios ofrecidos download: Descargar - user_list: - title: Listado usuarios + download_all: Descargar todo shared: movements: delete_reason: "¿Está seguro de borrar este comentario?" diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 9b8eea255..aff3fbb19 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -293,6 +293,7 @@ eu: required_field: Derrigorrez bete beharrekoa save: Gorde search: Bilatu + search_location: show: Erakutsi source_destination: Jatorria/ helmuga statistics: Estatistika @@ -309,6 +310,7 @@ eu: layouts: application: about: TimeOverflowri buruz + bdtnear: edit_org: " %{organization} aldaketak egin" edit_profile: Nire profila aldatu help: Laguntza @@ -377,6 +379,7 @@ eu: new: Banku berria show: contact_information: kontaktuaren informazioa + join_timebank: pages: about: app-mobile: Mugikorrerako Appa @@ -412,11 +415,8 @@ eu: show: info: "%{type} hau %{organization}(e)ri dagokio." reports: - cat_with_users: - title: Eskaintzen diren zerbitzuak download: Jaitsi - user_list: - title: Erabiltzaile zerrenda + download_all: shared: movements: delete_reason: Zihur zaude azalpen hau ezabatu nahi duzula? diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 5316ba0a6..e24ed1004 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -293,6 +293,7 @@ gl: required_field: "* Campo obrigatorio" save: Gardar search: Busca + search_location: show: Amosar source_destination: Dende/ata statistics: Estatísticas @@ -309,6 +310,7 @@ gl: layouts: application: about: Sobre TimeOverflow + bdtnear: edit_org: Actualización %{organization} edit_profile: Actualiza o meu perfil help: Axuda @@ -377,6 +379,7 @@ gl: new: Novo banco show: contact_information: Información de contacto + join_timebank: pages: about: app-mobile: Aplicación móbil @@ -412,11 +415,8 @@ gl: show: info: Este %{type} pertence a %{organization}. reports: - cat_with_users: - title: Servizos ofrecidos download: Descarga - user_list: - title: Lista de persoas usuarias + download_all: shared: movements: delete_reason: Estás seguro/a de borrar este comentario? diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 2cd7b0755..50d54dfa6 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -293,6 +293,7 @@ pt-BR: required_field: "* Campo obrigatório" save: Guardar search: Pesquisar + search_location: show: Mostrar source_destination: Origem/Destino statistics: Estatísticas @@ -309,6 +310,7 @@ pt-BR: layouts: application: about: Sobre TimeOverflow + bdtnear: edit_org: Modificar %{organization} edit_profile: Modificar meu perfil help: Ajuda @@ -377,6 +379,7 @@ pt-BR: new: Novo banco show: contact_information: Informação de contato + join_timebank: pages: about: app-mobile: Aplicativo móvel @@ -412,11 +415,8 @@ pt-BR: show: info: Este %{type} pertence a %{organization}. reports: - cat_with_users: - title: Serviços oferecidos download: Baixar - user_list: - title: Lista de usuários + download_all: shared: movements: delete_reason: Tem certeza de que quer apagar este comentário? diff --git a/config/routes.rb b/config/routes.rb index cc8fd4c7e..a88d0ee40 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,6 +64,7 @@ get "offer_list" => :post_list, type: "offer" get "inquiry_list" => :post_list, type: "inquiry" get "transfer_list" + get "download_all" end end diff --git a/spec/controllers/organizations_controller_spec.rb b/spec/controllers/organizations_controller_spec.rb index e0ae5a717..20d71361f 100644 --- a/spec/controllers/organizations_controller_spec.rb +++ b/spec/controllers/organizations_controller_spec.rb @@ -2,12 +2,64 @@ let!(:organization) { Fabricate(:organization) } let(:member) { Fabricate(:member, organization: organization) } let(:user) { member.user } + let!(:second_organization) { Fabricate(:organization) } describe 'GET #index' do - it 'populates and array of organizations' do - get :index + context 'without parameters' do + it 'populates and array of organizations' do + get :index - expect(assigns(:organizations)).to eq([organization]) + expect(assigns(:organizations)).to eq([organization, second_organization]) + end + end + + context 'a search is made' do + before do + second_organization.name = "Banco del tiempo Doe" + second_organization.city = "Sevilla" + second_organization.address = "Calle gloria" + second_organization.neighborhood = "La paz" + second_organization.save! + + organization.neighborhood = "La paz" + organization.save! + end + + it 'populates an array of organizations searching by city' do + get :index, params: { q: 'Sevilla' } + + expect(assigns(:organizations)).to eq([second_organization]) + end + + it 'populates an array of organizations searching by name' do + get :index, params: { q: 'Doe' } + + expect(assigns(:organizations)).to eq([second_organization]) + end + + it 'populates an array of organizations searching by address' do + get :index, params: { q: 'gloria' } + + expect(assigns(:organizations)).to eq([second_organization]) + end + + it 'populates an array of organizations searching by neighborhood' do + get :index, params: { q: 'Paz' } + + expect(assigns(:organizations)).to eq([organization, second_organization]) + end + + it 'allows to search by partial word' do + get :index, params: { q: 'Sev' } + + expect(assigns(:organizations)).to eq([second_organization]) + end + + it 'populates an array of organizations ignoring accents' do + get :index, params: { q: 'Sevillá' } + + expect(assigns(:organizations)).to eq([second_organization]) + end end end diff --git a/spec/controllers/reports_controller_spec.rb b/spec/controllers/reports_controller_spec.rb index fc6a1688d..369402369 100644 --- a/spec/controllers/reports_controller_spec.rb +++ b/spec/controllers/reports_controller_spec.rb @@ -89,5 +89,24 @@ expect(response.media_type).to eq("application/pdf") end end + + describe 'GET #download_all' do + it 'downloads a zip' do + get :download_all + + expect(response.media_type).to eq('application/zip') + expect(response.body).to include('Inquiries') + expect(response.body).to include('Offers') + expect(response.body).to include('Members') + expect(response.body).to include('Transfers') + end + + it 'redirects to download_all_report_path (retry) if zip is not ready' do + allow(subject).to receive(:add_csv_to_zip).and_raise(Errno::ENOENT) + get :download_all + + expect(response).to redirect_to(download_all_report_path) + end + end end end diff --git a/spec/fabricators/member_fabricator.rb b/spec/fabricators/member_fabricator.rb index 2ccb0e8e1..82e63d64b 100644 --- a/spec/fabricators/member_fabricator.rb +++ b/spec/fabricators/member_fabricator.rb @@ -1,8 +1,6 @@ Fabricator(:member) do - user { Fabricate(:user) } organization { Fabricate(:organization) } manager false active true - end diff --git a/spec/fabricators/post_fabricator.rb b/spec/fabricators/post_fabricator.rb index 919bd56b4..c5871867c 100644 --- a/spec/fabricators/post_fabricator.rb +++ b/spec/fabricators/post_fabricator.rb @@ -1,15 +1,12 @@ Fabricator(:post) do - title { Faker::Lorem.sentence } user { Fabricate(:user) } description { Faker::Lorem.paragraph } category { Fabricate(:category) } active { true } - end Fabricator(:inquiry) do - type "Inquiry" title { Faker::Lorem.sentence } @@ -21,7 +18,6 @@ end Fabricator(:offer) do - type "Offer" title { Faker::Lorem.sentence } @@ -29,5 +25,4 @@ description { Faker::Lorem.paragraph } category { Fabricate(:category) } active { true } - end diff --git a/spec/models/taggable_spec.rb b/spec/models/taggable_spec.rb index 80f420051..542c1a3dd 100644 --- a/spec/models/taggable_spec.rb +++ b/spec/models/taggable_spec.rb @@ -1,6 +1,7 @@ RSpec.describe Taggable do let(:organization) { Fabricate(:organization) } - + let(:tags) { %w(foo bar baz) } + let(:more_tags) { %w(foo baz qux) } let!(:offer) do Fabricate( :offer, @@ -17,9 +18,6 @@ end context "class methods and scopes" do - let(:tags) { %w(foo bar baz) } - let(:more_tags) { %w(foo baz qux) } - it "tagged_with" do expect(Offer.tagged_with("bar")).to eq [offer] end @@ -33,18 +31,28 @@ expect(Offer.find_like_tag("Foo")).to eq ["foo"] expect(Offer.find_like_tag("none")).to eq [] end - end - describe '.alphabetical_grouped_tags' do - let(:tags) { %w(foo bar baz Boo) } - let(:more_tags) { %w(foo baz qux) } + describe '.alphabetical_grouped_tags' do + let(:tags) { %w(foo bar baz Boo) } + let(:more_tags) { %w(foo baz qux) } - it 'sorts them by alphabetical order case insensitive' do - expect(Offer.alphabetical_grouped_tags).to eq({ - 'B' => [['bar', 1], ['baz', 2], ['Boo', 1]], - 'F' => [['foo', 2]], - 'Q' => [['qux', 1]] - }) + it 'sorts them by alphabetical order case insensitive' do + expect(Offer.alphabetical_grouped_tags).to eq({ + 'B' => [['bar', 1], ['baz', 2], ['Boo', 1]], + 'F' => [['foo', 2]], + 'Q' => [['qux', 1]] + }) + end end end + + it "#tag_list= writter accepts string and array" do + offer = Offer.new + + offer.tag_list = ["a", "b"] + expect(offer.tag_list).to eq "a, b" + + offer.tag_list = "c, d" + expect(offer.tag_list).to eq "c, d" + end end