diff --git a/.travis.yml b/.travis.yml index 4eb86bbf6..9da8210d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,10 @@ before_script: - bundle exec rake db:setup script: - "bundle exec rake assets:precompile RAILS_ENV=test" - - "bundle exec rake knapsack:rspec" + - "bin/knapsack_pro_rspec" env: global: - - CI_NODE_TOTAL=2 + - KNAPSACK_PRO_CI_NODE_TOTAL=2 matrix: - - CI_NODE_INDEX=0 - - CI_NODE_INDEX=1 + - KNAPSACK_PRO_CI_NODE_INDEX=0 + - KNAPSACK_PRO_CI_NODE_INDEX=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a487eadb4..2f39815c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,148 @@ -### 0.9 - 2017-06-15 +# Changelog +All notable changes to this project will be documented in this file. -* New features - * Budgets - * Basic polls - * Collaborative legistlation - * Custom pages - * GraphQL API - * Improved admin section -* Enhancements - * Improved admin section - * Rails 4.2.8 - * Ruby 2.3.2 -* Bug fixes - * CKEditor locale compilation fixed - * Fixed bugs in mobile layouts -* Deprecations - * SpendingProposals are deprecated now in favor of Budgets +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -### 0.8 - 2016-07-21 +## [Unreleased](https://github.com/consul/consul/compare/v0.11...consul:master) -* New features - * Support for customization schema, vía specific custom files, assets and folders -* Enhancements - * Rails 2.4.7 - * Ruby 2.3.1 -* Bug fixes - * Fixed bug causing errors on user deletion +## [0.11.0](https://github.com/consul/consul/compare/v0.10...v0.11) - 2017-12-05 -### 0.7 - 2016-04-25 +### Added +- Allow social media image meta tags to be overwritten https://github.com/consul/consul/pull/1756 & https://github.com/consul/consul/pull/2153 +- Allow users to verify their account against a local Census https://github.com/consul/consul/pull/1752 +- Make Proposals & Budgets Investments followable by users https://github.com/consul/consul/pull/1727 +- Show user followable activity on public user page https://github.com/consul/consul/pull/1750 +- Add Budget results view & table https://github.com/consul/consul/pull/1748 +- Improved Budget winners calculations https://github.com/consul/consul/pull/1738 +- Allow Documents to be uploaded to Proposals and Budget Investments https://github.com/consul/consul/pull/1809 +- Allow Communities creation on Proposals and Budget Investments (Run rake task 'communities:associate_community') https://github.com/consul/consul/pull/1815 https://github.com/consul/consul/pull/1833 +- Allow user to geolocate Proposals and Budget Investments on a map https://github.com/consul/consul/pull/1864 +- Legislation Process Proposals https://github.com/consul/consul/pull/1906 +- Autocomplete user tags https://github.com/consul/consul/pull/1905 +- GraphQL API docs https://github.com/consul/consul/pull/1763 +- Show recommended proposals and debates to users based in their interests https://github.com/consul/consul/pull/1824 +- Allow images & videos to be added to Poll questions https://github.com/consul/consul/pull/1835 https://github.com/consul/consul/pull/1915 +- Add Poll Shifts, to soon replace Poll OfficerAssignments usage entirely (for now just partially) +- Added dropdown menu for advanced users https://github.com/consul/consul/pull/1761 +- Help text headers and footers https://github.com/consul/consul/pull/1807 +- Added a couple of steps for linux installation guidelines https://github.com/consul/consul/pull/1846 +- Added TotalResult model, to replace Poll::FinalRecount https://github.com/consul/consul/pull/1866 1885 +- Preview Budget Results by admins https://github.com/consul/consul/pull/1923 +- Added comments to Polls https://github.com/consul/consul/pull/1961 +- Added images & videos to Polls https://github.com/consul/consul/pull/1990 https://github.com/consul/consul/pull/1989 +- Poll Answers are orderable now https://github.com/consul/consul/pull/2037 +- Poll Booth Assigment management https://github.com/consul/consul/pull/2087 +- Legislation processes documents https://github.com/consul/consul/pull/2084 +- Poll results https://github.com/consul/consul/pull/2082 +- Poll stats https://github.com/consul/consul/pull/2075 +- Poll stats on admin panel https://github.com/consul/consul/pull/2102 +- Added investment user tags admin interface https://github.com/consul/consul/pull/2068 +- Added Poll comments to GraphQL API https://github.com/consul/consul/pull/2148 +- Added option to unassign Valuator role https://github.com/consul/consul/pull/2110 +- Added search by name/email on several Admin sections https://github.com/consul/consul/pull/2105 +- Added Docker support https://github.com/consul/consul/pull/2127 & documentation https://consul_docs.gitbooks.io/docs/content/en/getting_started/docker.html +- Added population restriction validation on Budget Headings https://github.com/consul/consul/pull/2115 +- Added a `/consul.json` route that returns installation details (current release version and feature flags status) for a future dashboard app https://github.com/consul/consul/pull/2164 -* New features - * Debates - * Proposals - * Basic Spending Proposals -* Enhancements - * Rails 2.4.6 - * Ruby 2.2.3 \ No newline at end of file +### Changed +- Gem versions locked & cleanup https://github.com/consul/consul/pull/1730 +- Upgraded many minor versions https://github.com/consul/consul/pull/1747 +- Rails 4.2.10 https://github.com/consul/consul/pull/2128 +- Updated Code of Conduct to use contributor covenant 1.4 https://github.com/consul/consul/pull/1733 +- Improved consistency to all "Go back" buttons https://github.com/consul/consul/pull/1770 +- New CONSUL brand https://github.com/consul/consul/pull/1808 +- Admin panel redesign https://github.com/consul/consul/pull/1875 https://github.com/consul/consul/pull/2060 +- Swapped Poll White/Null/Total Results for Poll Recount https://github.com/consul/consul/pull/1963 +- Improved Poll index view https://github.com/consul/consul/pull/1959 https://github.com/consul/consul/pull/1987 +- Update secrets and deploy secrets example files https://github.com/consul/consul/pull/1966 +- Improved Poll Officer panel features +- Consistency across all admin profiles sections https://github.com/consul/consul/pull/2089 +- Improved dev_seeds with more Poll content https://github.com/consul/consul/pull/2121 +- Comment count now updates live after publishing a new one https://github.com/consul/consul/pull/2090 + +### Removed +- Removed Tolk gem usage, we've moved to Crowdin service https://github.com/consul/consul/pull/1729 +- Removed Polls manual recounts (model Poll::FinalRecount) https://github.com/consul/consul/pull/1764 +- Skipped specs for deprecated Spending Proposal model https://github.com/consul/consul/pull/1773 +- Moved Documentation to https://github.com/consul/docs https://github.com/consul/consul/pull/1861 +- Remove Poll Officer recounts, add Final & Totals votes https://github.com/consul/consul/pull/1919 +- Remove deprecated Poll results models https://github.com/consul/consul/pull/1964 +- Remove deprecated Poll::Question valid_answers attribute & usage https://github.com/consul/consul/pull/2073 https://github.com/consul/consul/pull/2074 + +### Fixed +- Foundation settings stylesheet https://github.com/consul/consul/pull/1766 +- Budget milestone date localization https://github.com/consul/consul/pull/1734 +- Return datetime format for en locale https://github.com/consul/consul/pull/1795 +- Show bottom proposals button only if proposals exists https://github.com/consul/consul/pull/1798 +- Check SMS verification in a more consistent way https://github.com/consul/consul/pull/1832 +- Allow only YouTube/Vimeo URLs on 'video_url' attributes https://github.com/consul/consul/pull/1854 +- Remove empty comments html https://github.com/consul/consul/pull/1862 +- Fixed admin/poll routing errors https://github.com/consul/consul/pull/1863 +- Display datepicker arrows https://github.com/consul/consul/pull/1869 +- Validate presence poll presence on Poll::Question creation https://github.com/consul/consul/pull/1868 +- Switch flag/unflag buttons on use via ajax https://github.com/consul/consul/pull/1883 +- Flaky specs fixed https://github.com/consul/consul/pull/1888 +- Fixed link back from moderation dashboard to root_path https://github.com/consul/consul/pull/2132 +- Fixed Budget random pagination order https://github.com/consul/consul/pull/2131 +- Fixed `direct_messages_max_per_day` set to nil https://github.com/consul/consul/pull/2100 +- Fixed notification link error when someone commented a Topic https://github.com/consul/consul/pull/2094 +- Lots of small UI/UX/SEO/SEM improvements + +## [0.10.0](https://github.com/consul/consul/compare/v0.9...v0.10) - 2017-07-05 +### Added +- Milestones on Budget Investment's +- Feature flag to enable/disable Legislative Processes +- Locale site pages customization +- Incompatible investments + +### Changed +- Localization files reorganization. Check migration instruction at https://github.com/consul/consul/releases/tag/v0.10 +- Rails 4.2.9 + +## [0.9.0](https://github.com/consul/consul/compare/v0.8...v0.9) - 2017-06-15 +### Added +- Budgets +- Basic polls +- Collaborative legistlation +- Custom pages +- GraphQL API +- Improved admin section + +### Changed +- Improved admin section +- Rails 4.2.8 +- Ruby 2.3.2 + +### Deprecated +- SpendingProposals are deprecated now in favor of Budgets + +### Fixed +- CKEditor locale compilation fixed +- Fixed bugs in mobile layouts + +## [0.8.0](https://github.com/consul/consul/compare/v0.7...v0.8)- 2016-07-21 +### Added +- Support for customization schema, vía specific custom files, assets and folders + +### Changed +- Rails 4.2.7 +- Ruby 2.3.1 + +### Fixed +- Fixed bug causing errors on user deletion + +## [0.7.0] - 2016-04-25 +### Added +- Debates +- Proposals +- Basic Spending Proposals + +### Changed +- Rails 4.2.6 +- Ruby 2.2.3 + +[Unreleased]: https://github.com/consul/consul/compare/v0.10...consul:master +[0.10.0]: https://github.com/consul/consul/compare/v0.9...v0.10 +[0.9.0]: https://github.com/consul/consul/compare/v0.8...v0.9 +[0.8.0]: https://github.com/consul/consul/compare/v0.7...v0.8 diff --git a/Gemfile b/Gemfile index b0c7f2e9a..94002104e 100644 --- a/Gemfile +++ b/Gemfile @@ -64,7 +64,7 @@ group :development, :test do gem 'factory_girl_rails', '~> 4.8.0' gem 'faker', '~> 1.7.3' gem 'i18n-tasks', '~> 0.9.15' - gem 'knapsack', '~> 1.13.3' + gem 'knapsack_pro', '~> 0.53.0' gem 'launchy', '~> 2.4.3' gem 'letter_opener_web', '~> 1.3.1' gem 'quiet_assets', '~> 1.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 8bfbbb27c..d7d210576 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -229,9 +229,8 @@ GEM kaminari-core (= 1.0.1) kaminari-core (1.0.1) kgio (2.11.0) - knapsack (1.13.3) + knapsack_pro (0.53.0) rake - timecop (>= 0.1.0) kramdown (1.14.0) launchy (2.4.3) addressable (~> 2.3) @@ -451,7 +450,6 @@ GEM thread (0.2.2) thread_safe (0.3.6) tilt (2.0.7) - timecop (0.9.1) tins (1.15.0) turbolinks (2.5.3) coffee-rails @@ -531,7 +529,7 @@ DEPENDENCIES jquery-rails (~> 4.3.1) jquery-ui-rails (~> 6.0.1) kaminari (~> 1.0.1) - knapsack (~> 1.13.3) + knapsack_pro (~> 0.53.0) launchy (~> 2.4.3) letter_opener_web (~> 1.3.1) mdl (~> 0.4.0) @@ -573,4 +571,4 @@ DEPENDENCIES whenever (~> 0.9.7) BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/Rakefile b/Rakefile index ef2c381ff..13a99536b 100644 --- a/Rakefile +++ b/Rakefile @@ -4,4 +4,4 @@ require File.expand_path('../config/application', __FILE__) Rails.application.load_tasks -Knapsack.load_tasks if defined?(Knapsack) \ No newline at end of file +KnapsackPro.load_tasks if defined?(KnapsackPro) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 51de9c678..611ecc546 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -73,6 +73,7 @@ //= require map //= require polls //= require sortable +//= require table_sortable var initialize_modules = function() { App.Comments.initialize(); @@ -113,6 +114,7 @@ var initialize_modules = function() { App.Map.initialize(); App.Polls.initialize(); App.Sortable.initialize(); + App.TableSortable.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/table_sortable.js.coffee b/app/assets/javascripts/table_sortable.js.coffee new file mode 100644 index 000000000..331f794a4 --- /dev/null +++ b/app/assets/javascripts/table_sortable.js.coffee @@ -0,0 +1,23 @@ +App.TableSortable = + getCellValue: (row, index) -> + $(row).children('td').eq(index).text() + + comparer: (index) -> + (a, b) -> + valA = App.TableSortable.getCellValue(a, index) + valB = App.TableSortable.getCellValue(b, index) + return if $.isNumeric(valA) and $.isNumeric(valB) then valA - valB else valA.localeCompare(valB) + + initialize: -> + $('table.sortable th').click -> + table = $(this).parents('table').eq(0) + rows = table.find('tr:gt(0)').not('tfoot tr').toArray().sort(App.TableSortable.comparer($(this).index())) + @asc = !@asc + if !@asc + rows = rows.reverse() + i = 0 + while i < rows.length + table.append rows[i] + i++ + return + \ No newline at end of file diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 85997aecd..a2e3e0e7f 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -270,6 +270,11 @@ $sidebar-active: #f4fcd0; } } +.sortable thead th:hover { + text-decoration: underline; + cursor: pointer; +} + // 02. Sidebar // ----------- diff --git a/app/controllers/admin/budget_investments_controller.rb b/app/controllers/admin/budget_investments_controller.rb index dd10930fb..4b6bd6b3a 100644 --- a/app/controllers/admin/budget_investments_controller.rb +++ b/app/controllers/admin/budget_investments_controller.rb @@ -51,7 +51,7 @@ class Admin::BudgetInvestmentsController < Admin::BaseController def budget_investment_params params.require(:budget_investment) - .permit(:title, :description, :external_url, :heading_id, :administrator_id, :valuation_tag_list, :incompatible, + .permit(:title, :description, :external_url, :heading_id, :administrator_id, :tag_list, :valuation_tag_list, :incompatible, :selected, valuator_ids: []) end diff --git a/app/controllers/admin/stats_controller.rb b/app/controllers/admin/stats_controller.rb index 5a50d03b4..91e266b77 100644 --- a/app/controllers/admin/stats_controller.rb +++ b/app/controllers/admin/stats_controller.rb @@ -36,4 +36,9 @@ class Admin::StatsController < Admin::BaseController @users_who_have_sent_message = DirectMessage.select(:sender_id).distinct.count end -end \ No newline at end of file + def polls + @polls = ::Poll.current + @participants = ::Poll::Voter.where(poll: @polls) + end + +end diff --git a/app/controllers/admin/valuators_controller.rb b/app/controllers/admin/valuators_controller.rb index d567a38b2..8b454a1a2 100644 --- a/app/controllers/admin/valuators_controller.rb +++ b/app/controllers/admin/valuators_controller.rb @@ -19,6 +19,11 @@ class Admin::ValuatorsController < Admin::BaseController redirect_to admin_valuators_path end + def destroy + @valuator.destroy + redirect_to admin_valuators_path + end + def summary @valuators = Valuator.order(spending_proposals_count: :desc) end diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index bb00f24af..5e7c1ddf0 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -28,8 +28,8 @@ module Budgets respond_to :html, :js def index - @investments = @investments.apply_filters_and_search(@budget, params, @current_filter) - .send("sort_by_#{@current_order}").page(params[:page]).per(10).for_render + @investments = investments.page(params[:page]).per(10).for_render + @investment_ids = @investments.pluck(:id) load_investment_votes(@investments) @tag_cloud = tag_cloud @@ -94,9 +94,8 @@ module Budgets def set_random_seed if params[:order] == 'random' || params[:order].blank? - params[:random_seed] ||= rand(99) / 100.0 - seed = Float(params[:random_seed]) rescue 0 - Budget::Investment.connection.execute("select setseed(#{seed})") + seed = rand(-100..100) / 100.0 + params[:random_seed] ||= Float(seed) rescue 0 else params[:random_seed] = nil end @@ -131,6 +130,16 @@ module Budgets TagCloud.new(Budget::Investment, params[:search]) end + def investments + if @current_order == 'random' + @investments.apply_filters_and_search(@budget, params, @current_filter) + .send("sort_by_#{@current_order}", params[:random_seed]) + else + @investments.apply_filters_and_search(@budget, params, @current_filter) + .send("sort_by_#{@current_order}") + end + end + end end diff --git a/app/controllers/installation_controller.rb b/app/controllers/installation_controller.rb new file mode 100644 index 000000000..a0bc0aa74 --- /dev/null +++ b/app/controllers/installation_controller.rb @@ -0,0 +1,24 @@ +class InstallationController < ApplicationController + + skip_authorization_check + + def details + respond_to do |format| + format.any { render json: consul_installation_details.to_json, content_type: 'application/json' } + end + end + + private + + def consul_installation_details + { + release: 'v0.11' + }.merge(features: settings_feature_flags) + end + + def settings_feature_flags + Setting.where("key LIKE 'feature.%'").each_with_object({}) { |x, n| n[x.key.remove('feature.')] = x.value } + end + +end + diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 369cabe84..3191a2ab9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,4 +60,8 @@ module ApplicationHelper def format_price(number) number_to_currency(number, precision: 0, locale: I18n.default_locale) end + + def kaminari_path(url) + "#{root_url.chomp("\/")}#{url}" + end end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 951f06249..3f136f913 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -43,7 +43,7 @@ module Abilities can [:search, :create, :index, :destroy], ::Administrator can [:search, :create, :index, :destroy], ::Moderator - can [:search, :create, :index, :summary], ::Valuator + can [:search, :create, :index, :destroy, :summary], ::Valuator can [:search, :create, :index, :destroy], ::Manager can [:search, :index], ::User diff --git a/app/models/budget/heading.rb b/app/models/budget/heading.rb index 1a232c75e..d740db502 100644 --- a/app/models/budget/heading.rb +++ b/app/models/budget/heading.rb @@ -10,6 +10,7 @@ class Budget validates :name, presence: true, uniqueness: { if: :name_exists_in_budget_headings } validates :price, presence: true validates :slug, presence: true, format: /\A[a-z0-9\-_]+\z/ + validates :population, numericality: { greater_than: 0 }, allow_nil: true delegate :budget, :budget_id, to: :group, allow_nil: true diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index ddf6e819c..173a7fd36 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -17,6 +17,7 @@ class Budget acts_as_votable acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Relationable belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :heading @@ -43,7 +44,7 @@ class Budget scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) } scope :sort_by_ballots, -> { reorder(ballot_lines_count: :desc, id: :desc) } scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :desc, id: :desc) } - scope :sort_by_random, -> { reorder("RANDOM()") } + scope :sort_by_random, ->(seed) { reorder("budget_investments.id % #{seed.to_f&.positive? ? seed : 1}, budget_investments.id") } scope :valuation_open, -> { where(valuation_finished: false) } scope :without_admin, -> { valuation_open.where(administrator_id: nil) } diff --git a/app/models/comment.rb b/app/models/comment.rb index 8e3024d06..ccfc04602 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -30,9 +30,11 @@ class Comment < ActiveRecord::Base scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :public_for_api, -> do where(%{(comments.commentable_type = 'Debate' and comments.commentable_id in (?)) or - (comments.commentable_type = 'Proposal' and comments.commentable_id in (?))}, + (comments.commentable_type = 'Proposal' and comments.commentable_id in (?)) or + (comments.commentable_type = 'Poll' and comments.commentable_id in (?))}, Debate.public_for_api.pluck(:id), - Proposal.public_for_api.pluck(:id)) + Proposal.public_for_api.pluck(:id), + Poll.public_for_api.pluck(:id)) end scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) } diff --git a/app/models/concerns/relationable.rb b/app/models/concerns/relationable.rb new file mode 100644 index 000000000..26f755f03 --- /dev/null +++ b/app/models/concerns/relationable.rb @@ -0,0 +1,23 @@ +module Relationable + extend ActiveSupport::Concern + + included do + has_many :related_contents, as: :parent_relationable, dependent: :destroy + end + + def relate_content(relationable) + RelatedContent.find_or_create_by(parent_relationable: self, child_relationable: relationable) + end + + def relationed_contents + related_contents.not_hidden.map { |related_content| related_content.child_relationable } + end + + def report_related_content(relationable) + related_content = related_contents.find_by(child_relationable: relationable) + if related_content.present? + related_content.increment!(:times_reported) + related_content.opposite_related_content.increment!(:times_reported) + end + end +end diff --git a/app/models/debate.rb b/app/models/debate.rb index 65c06345d..e949e8092 100644 --- a/app/models/debate.rb +++ b/app/models/debate.rb @@ -9,6 +9,7 @@ class Debate < ActiveRecord::Base include Filterable include HasPublicAuthor include Graphqlable + include Relationable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/poll.rb b/app/models/poll.rb index 27125d8a1..ffb4e36f4 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -28,6 +28,7 @@ class Poll < ActiveRecord::Base scope :recounting, -> { Poll.where(ends_at: (Date.current.beginning_of_day - RECOUNT_DURATION)..Date.current.beginning_of_day) } scope :published, -> { where('published = ?', true) } scope :by_geozone_id, ->(geozone_id) { where(geozones: {id: geozone_id}.joins(:geozones)) } + scope :public_for_api, -> { all } scope :sort_for_list, -> { order(:geozone_restricted, :starts_at, :name) } diff --git a/app/models/poll/booth_assignment.rb b/app/models/poll/booth_assignment.rb index 8489c3cf0..81759ee0f 100644 --- a/app/models/poll/booth_assignment.rb +++ b/app/models/poll/booth_assignment.rb @@ -3,10 +3,26 @@ class Poll belongs_to :booth belongs_to :poll + before_destroy :destroy_poll_shifts, only: :destroy + has_many :officer_assignments, class_name: "Poll::OfficerAssignment", dependent: :destroy has_many :officers, through: :officer_assignments has_many :voters has_many :partial_results has_many :recounts + + def shifts? + shifts.empty? ? false : true + end + + private + + def shifts + Poll::Shift.where(booth_id: booth_id, officer_id: officer_assignments.pluck(:officer_id), date: officer_assignments.pluck(:date)) + end + + def destroy_poll_shifts + shifts.destroy_all + end end end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 3b0696d26..aaf5bea87 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -17,6 +17,7 @@ class Proposal < ActiveRecord::Base max_file_size: 3.megabytes, accepted_content_types: [ "application/pdf" ] include EmbedVideosHelper + include Relationable acts_as_votable acts_as_paranoid column: :hidden_at diff --git a/app/models/related_content.rb b/app/models/related_content.rb new file mode 100644 index 000000000..ab72aefe3 --- /dev/null +++ b/app/models/related_content.rb @@ -0,0 +1,33 @@ +class RelatedContent < ActiveRecord::Base + RELATED_CONTENTS_REPORT_THRESHOLD = Setting['related_contents_report_threshold'].to_i + + belongs_to :parent_relationable, polymorphic: true + belongs_to :child_relationable, polymorphic: true + has_one :opposite_related_content, class_name: 'RelatedContent', foreign_key: :related_content_id + + validates :parent_relationable_id, presence: true + validates :parent_relationable_type, presence: true + validates :child_relationable_id, presence: true + validates :child_relationable_type, presence: true + validates :parent_relationable_id, uniqueness: { scope: [:parent_relationable_type, :child_relationable_id, :child_relationable_type] } + + after_create :create_opposite_related_content, unless: proc { opposite_related_content.present? } + after_destroy :destroy_opposite_related_content, if: proc { opposite_related_content.present? } + + scope :not_hidden, -> { where('times_reported <= ?', RELATED_CONTENTS_REPORT_THRESHOLD) } + + def hidden_by_reports? + times_reported > RELATED_CONTENTS_REPORT_THRESHOLD + end + + private + + def create_opposite_related_content + related_content = RelatedContent.create!(opposite_related_content: self, parent_relationable: child_relationable, child_relationable: parent_relationable) + self.opposite_related_content = related_content + end + + def destroy_opposite_related_content + opposite_related_content.destroy + end +end diff --git a/app/views/admin/budget_investments/_written_by_author.html.erb b/app/views/admin/budget_investments/_written_by_author.html.erb index a913aff00..8cf2eca57 100644 --- a/app/views/admin/budget_investments/_written_by_author.html.erb +++ b/app/views/admin/budget_investments/_written_by_author.html.erb @@ -56,3 +56,8 @@ <% end %> <%= safe_html_with_links @investment.description %> + +

+ <%= t("admin.budget_investments.show.user_tags") %>: + <%= @investment.tag_list.sort.join(', ') %> +

diff --git a/app/views/admin/budget_investments/edit.html.erb b/app/views/admin/budget_investments/edit.html.erb index c33039021..ab843cf24 100644 --- a/app/views/admin/budget_investments/edit.html.erb +++ b/app/views/admin/budget_investments/edit.html.erb @@ -18,6 +18,13 @@ <%= f.cktext_area :description, maxlength: Budget::Investment.description_max_length, ckeditor: { language: I18n.locale } %> +
+ <%= f.label :tag_list, t("admin.budget_investments.edit.user_tags") %> + <%= f.text_field :tag_list, + value: @investment.tag_list.sort.join(','), + label: false %> +
+
<%= f.text_field :external_url %>
@@ -39,7 +46,7 @@
- <%= f.label :tag_list, t("admin.budget_investments.edit.tags") %> + <%= f.label :valuation_tag_list, t("admin.budget_investments.edit.tags") %>
<% @tags.each do |tag| %> <%= tag.name %> diff --git a/app/views/admin/budget_investments/show.html.erb b/app/views/admin/budget_investments/show.html.erb index f220c0955..5fa35a1fa 100644 --- a/app/views/admin/budget_investments/show.html.erb +++ b/app/views/admin/budget_investments/show.html.erb @@ -19,7 +19,7 @@

<%= t("admin.budget_investments.show.tags") %>: - <%= @investment.tags_on(:valuation).pluck(:name).join(', ') %> + <%= @investment.tags_on(:valuation).pluck(:name).sort.join(', ') %>

diff --git a/app/views/admin/budgets/_group.html.erb b/app/views/admin/budgets/_group.html.erb index 8537fa477..3d65730cc 100644 --- a/app/views/admin/budgets/_group.html.erb +++ b/app/views/admin/budgets/_group.html.erb @@ -49,10 +49,17 @@

- <%= f.text_field :population, + <%= f.number_field :population, label: false, maxlength: 8, - placeholder: t("admin.budgets.form.population") %> + min: 1, + placeholder: t("admin.budgets.form.population"), + data: {toggle_focus: "population-info"} %> +
+
+
diff --git a/app/views/admin/poll/booth_assignments/_booth_assignment.html.erb b/app/views/admin/poll/booth_assignments/_booth_assignment.html.erb index d78f7be8b..d7896446f 100644 --- a/app/views/admin/poll/booth_assignments/_booth_assignment.html.erb +++ b/app/views/admin/poll/booth_assignments/_booth_assignment.html.erb @@ -14,7 +14,8 @@ method: :delete, remote: true, title: t("admin.booth_assignments.manage.actions.unassign"), - class: "button hollow alert" %> + class: "button hollow alert", + data: (booth_assignment.shifts? ? {confirm: "#{t("admin.poll_booth_assignments.alert.shifts")}"} : nil) if !@poll.expired? %> <% else %> @@ -26,6 +27,6 @@ method: :post, remote: true, title: t("admin.booth_assignments.manage.actions.assign"), - class: "button" %> + class: "button" if !@poll.expired? %> <% end %> diff --git a/app/views/admin/stats/polls.html.erb b/app/views/admin/stats/polls.html.erb new file mode 100644 index 000000000..c2b0b8481 --- /dev/null +++ b/app/views/admin/stats/polls.html.erb @@ -0,0 +1,87 @@ +<%= back_link_to %> + +

<%= t("admin.stats.polls.title")%>

+ +
+
+
+ +
+ +
+ +
+
+
+ +

<%= t("admin.stats.polls.all") %>

+ + + + + + + + + <% @polls.each do |poll| %> + + + + + + <% end %> +
<%= t("admin.stats.polls.table.poll_name") %><%= t("admin.stats.polls.total_participants") %><%= t("admin.stats.polls.table.origin_web") %>
+ <%= poll.name %> + + <%= poll.voters.select(:user_id).distinct.count %> + + <%= poll.voters.web.select(:user_id).distinct.count %> +
+ +<% @polls.each do |poll| %> +

+ <%= t("admin.stats.polls.poll_questions", poll: poll.name) %> +

+ + + + + + + + <% poll.questions.each do |question| %> + + + + + <% end %> + + + + + + +
<%= t("admin.stats.polls.table.question_name") %> + <%= t("admin.stats.polls.table.origin_web") %> +
+ <%= question.title %> + + <%= ::Poll::Answer.by_question(question).count %> +
+ + <%= t("admin.stats.polls.table.origin_total") %>: + <%= ::Poll::Answer.where(question: poll.questions) + .select(:author_id).distinct.count %> + +
+<% end %> \ No newline at end of file diff --git a/app/views/admin/stats/show.html.erb b/app/views/admin/stats/show.html.erb index 5df7b5df2..330a69b01 100644 --- a/app/views/admin/stats/show.html.erb +++ b/app/views/admin/stats/show.html.erb @@ -7,6 +7,8 @@

<%= t "admin.stats.show.stats_title" %>

+ <%= link_to t("admin.stats.show.polls"), + polls_admin_stats_path, class: "button hollow" %> <%= link_to t("admin.stats.show.direct_messages"), direct_messages_admin_stats_path, class: "button hollow" %> <%= link_to t("admin.stats.show.proposal_notifications"), diff --git a/app/views/admin/valuators/index.html.erb b/app/views/admin/valuators/index.html.erb index 622d01307..dfb93ed80 100644 --- a/app/views/admin/valuators/index.html.erb +++ b/app/views/admin/valuators/index.html.erb @@ -1,6 +1,6 @@

<%= t("admin.valuators.index.title") %>

-<%= render 'admin/shared/user_search', url: search_admin_valuators_path %> +<%= render "admin/shared/user_search", url: search_admin_valuators_path %>
<% if @valuators.any? %> @@ -11,16 +11,13 @@ <%= t("admin.valuators.index.name") %> <%= t("admin.valuators.index.email") %> <%= t("admin.valuators.index.description") %> + <%= t("admin.actions.actions") %> <% @valuators.each do |valuator| %> - - <%= valuator.name %> - - - <%= valuator.email %> - + <%= valuator.name %> + <%= valuator.email %> <% if valuator.description.present? %> <%= valuator.description %> @@ -28,6 +25,12 @@ <%= t("admin.valuators.index.no_description") %> <% end %> + + <%= link_to t("admin.valuators.valuator.delete"), + admin_valuator_path(valuator), + method: :delete, + class: "button hollow alert expanded" %> + <% end %> diff --git a/app/views/admin/valuators/search.html.erb b/app/views/admin/valuators/search.html.erb index 2710cb44d..b383e554d 100644 --- a/app/views/admin/valuators/search.html.erb +++ b/app/views/admin/valuators/search.html.erb @@ -29,7 +29,12 @@ <%= t("admin.valuators.index.no_description") %> <% end %> - <% unless user.valuator? %> + <% if user.valuator? %> + <%= link_to t("admin.valuators.valuator.delete"), + admin_valuator_path(user), + method: :delete, + class: "button hollow alert expanded" %> + <% else %> <%= form_for Valuator.new(user: user), url: admin_valuators_path do |f| %> <%= f.text_field :description, label: false, diff --git a/app/views/kaminari/_first_page.html.erb b/app/views/kaminari/_first_page.html.erb index b9802389d..e8afb0431 100644 --- a/app/views/kaminari/_first_page.html.erb +++ b/app/views/kaminari/_first_page.html.erb @@ -1,3 +1,3 @@
  • - <%= link_to t("views.pagination.first").html_safe, url, :remote => remote %> + <%= link_to t("views.pagination.first").html_safe, kaminari_path(url), :remote => remote %>
  • diff --git a/app/views/kaminari/_last_page.html.erb b/app/views/kaminari/_last_page.html.erb index 179f6e7fc..5a49bd7e2 100644 --- a/app/views/kaminari/_last_page.html.erb +++ b/app/views/kaminari/_last_page.html.erb @@ -1,3 +1,3 @@
  • - <%= link_to t("views.pagination.last").html_safe, url, :remote => remote %> + <%= link_to t("views.pagination.last").html_safe, kaminari_path(url), :remote => remote %>
  • diff --git a/app/views/kaminari/_next_page.html.erb b/app/views/kaminari/_next_page.html.erb index f305b1b7d..11c700900 100644 --- a/app/views/kaminari/_next_page.html.erb +++ b/app/views/kaminari/_next_page.html.erb @@ -1,3 +1,3 @@
  • - <%= link_to t("views.pagination.next").html_safe, url, :rel => "next", :remote => remote %> + <%= link_to t("views.pagination.next").html_safe, kaminari_path(url), :rel => "next", :remote => remote %>
  • diff --git a/app/views/kaminari/_page.html.erb b/app/views/kaminari/_page.html.erb index 12610dada..7b380fe4f 100644 --- a/app/views/kaminari/_page.html.erb +++ b/app/views/kaminari/_page.html.erb @@ -5,6 +5,6 @@ <% else %>
  • - <%= link_to page, url, {:remote => remote, :rel => page.next? ? "next" : page.prev? ? "prev" : nil} %> + <%= link_to page, kaminari_path(url), {:remote => remote, :rel => page.next? ? "next" : page.prev? ? "prev" : nil} %>
  • <% end %> diff --git a/app/views/kaminari/_prev_page.html.erb b/app/views/kaminari/_prev_page.html.erb index 902a8c89e..aba1d9369 100644 --- a/app/views/kaminari/_prev_page.html.erb +++ b/app/views/kaminari/_prev_page.html.erb @@ -1,3 +1,3 @@
  • - <%= link_to t("views.pagination.previous").html_safe, url, :rel => "prev", :remote => remote %> + <%= link_to t("views.pagination.previous").html_safe, kaminari_path(url), :rel => "prev", :remote => remote %>
  • diff --git a/app/views/management/_menu.html.erb b/app/views/management/_menu.html.erb index 050e5eb7f..b01bbaf0c 100644 --- a/app/views/management/_menu.html.erb +++ b/app/views/management/_menu.html.erb @@ -44,21 +44,31 @@ <% end %> -
  • > - <%= link_to create_investments_management_budgets_path do %> - - <%= t("management.menu.create_budget_investment") %> - <% end %> -
  • + <% if Setting['feature.budgets'] %> +
  • > + <%= link_to create_investments_management_budgets_path do %> + + <%= t("management.menu.create_budget_investment") %> + <% end %> +
  • -
  • > - <%= link_to support_investments_management_budgets_path do %> - - <%= t("management.menu.support_budget_investments") %> - <% end %> -
  • +
  • > + <%= link_to support_investments_management_budgets_path do %> + + <%= t("management.menu.support_budget_investments") %> + <% end %> +
  • + +
  • > + <%= link_to print_investments_management_budgets_path do %> + + <%= t("management.menu.print_budget_investments") %> + <% end %> +
  • + <% end %>
  • > <%= link_to print_management_proposals_path do %> @@ -74,15 +84,6 @@ <% end %>
  • -
  • > - <%= link_to print_investments_management_budgets_path do %> - - <%= t("management.menu.print_budget_investments") %> - <% end %> -
  • - -
  • <%= link_to new_management_user_invite_path do %> diff --git a/app/views/shared/_social_media_meta_tags.html.erb b/app/views/shared/_social_media_meta_tags.html.erb index 8c1196c96..a72ab250e 100644 --- a/app/views/shared/_social_media_meta_tags.html.erb +++ b/app/views/shared/_social_media_meta_tags.html.erb @@ -1,20 +1,20 @@ - - - - - + + + + + - + <% if setting['url'] %> - /> + <% end %> <% if setting['facebook_handle'] %> <% end %> - + - + diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index e27f4436e..de64d9cd8 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -4,6 +4,11 @@ <%= render "shared/canonical", href: root_url %> <% end %> +<% provide :social_media_meta_tags do %> + <%= render "shared/social_media_meta_tags", + social_url: root_url %> +<% end %> +
    diff --git a/bin/knapsack_pro_rspec b/bin/knapsack_pro_rspec new file mode 100755 index 000000000..9aaf762ad --- /dev/null +++ b/bin/knapsack_pro_rspec @@ -0,0 +1,9 @@ +#!/bin/bash +if [ "$KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC" = "" ]; then + KNAPSACK_PRO_ENDPOINT=https://api-disabled-for-fork.knapsackpro.com \ + KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC=disabled-for-fork \ + bundle exec rake knapsack_pro:rspec # use Regular Mode here always +else + # Queue Mode - dynamic tests allocation across CI nodes + bundle exec rake knapsack_pro:queue:rspec +fi diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml index f5f06122e..89befa33b 100644 --- a/config/locales/en/activerecord.yml +++ b/config/locales/en/activerecord.yml @@ -288,3 +288,8 @@ en: image: image_width: "Width must be %{required_width}px" image_height: "Height must be %{required_height}px" + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index 823d7084b..62dc77d19 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -104,6 +104,7 @@ en: table_heading: Heading table_amount: Amount table_population: Population + population_info: "Budget Heading population field is used for Statistic purposes at the end of the Budget to show for each Heading that represents an area with population what percentage voted. The field is optional so you can leave it empty if it doesn't apply." winners: calculate: Calculate Winner Investments calculated: Winners being calculated, it may take a minute. @@ -157,6 +158,7 @@ en: dossier: Dossier edit_dossier: Edit dossier tags: Tags + user_tags: User tags undefined: Undefined milestone: Milestone new_milestone: Create new milestone @@ -181,6 +183,7 @@ en: assigned_valuators: Valuators select_heading: Select heading submit_button: Update + user_tags: User assigned tags tags: Tags tags_placeholder: "Write the tags you want separated by commas (,)" undefined: Undefined @@ -481,6 +484,7 @@ en: valuator: description_placeholder: 'Description (optional)' add: Add to valuators + delete: Delete search: title: 'Valuators: User search' summary: @@ -555,6 +559,8 @@ en: assign: Assign booth unassign: Unassign booth poll_booth_assignments: + alert: + shifts: "There are shifts associated to this booth. If you remove the booth assignment, the shifts will be also deleted. Continue?" flash: destroy: "Booth not assigned anymore" create: "Booth assigned" @@ -952,6 +958,7 @@ en: direct_messages: Direct messages proposal_notifications: Proposal notifications incomplete_verifications: Incomplete verifications + polls: Polls direct_messages: title: Direct messages total: Total @@ -960,6 +967,17 @@ en: title: Proposal notifications total: Total proposals_with_notifications: Proposals with notifications + polls: + title: Poll Stats + all: Polls + web_participants: Web participants + total_participants: Total Participants + poll_questions: "Questions from poll: %{poll}" + table: + poll_name: Poll + question_name: Question + origin_web: Web participants + origin_total: Total participants tags: create: Create topic destroy: Destroy topic diff --git a/config/locales/en/settings.yml b/config/locales/en/settings.yml index 0d5862408..2295660ac 100644 --- a/config/locales/en/settings.yml +++ b/config/locales/en/settings.yml @@ -48,6 +48,7 @@ en: map_zoom: Zoom mailer_from_name: Origin email name mailer_from_address: Origin email address + meta_title: "Site title (SEO)" meta_description: "Site description (SEO)" meta_keywords: "Keywords (SEO)" verification_offices_url: Verification offices URL diff --git a/config/locales/es/activerecord.yml b/config/locales/es/activerecord.yml index 50499193d..16885db63 100644 --- a/config/locales/es/activerecord.yml +++ b/config/locales/es/activerecord.yml @@ -283,3 +283,8 @@ es: image: image_width: "Debe tener %{required_width}px de ancho" image_height: "Debe tener %{required_height}px de alto" + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes \ No newline at end of file diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index 8d0b6cbf8..a54cb9bcd 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -104,6 +104,7 @@ es: table_heading: Partida table_amount: Cantidad table_population: Población + population_info: "El campo población de las partidas presupuestarias se usa con fines estadísticos únicamente, con el objetivo de mostrar el porcentaje de votos habidos en cada partida que represente un área con población. Es un campo opcional, así que puedes dejarlo en blanco si no aplica." winners: calculate: Calcular propuestas ganadoras calculated: Calculando ganadoras, puede tardar un minuto. @@ -157,6 +158,7 @@ es: dossier: Informe edit_dossier: Editar informe tags: Etiquetas + user_tags: Etiquetas del usuario undefined: Sin definir milestone: Seguimiento new_milestone: Crear nuevo hito @@ -181,6 +183,7 @@ es: assigned_valuators: Evaluadores select_heading: Seleccionar partida submit_button: Actualizar + user_tags: Etiquetas asignadas por el usuario tags: Etiquetas tags_placeholder: "Escribe las etiquetas que desees separadas por comas (,)" undefined: Sin definir @@ -481,6 +484,7 @@ es: valuator: description_placeholder: 'Descripción (opcional)' add: Añadir como evaluador + delete: Borrar search: title: 'Evaluadores: Búsqueda de usuarios' summary: @@ -553,8 +557,10 @@ es: unassigned: No asignada actions: assign: Asignar urna - unassign: Asignar urna + unassign: Desasignar urna poll_booth_assignments: + alert: + shifts: "Hay turnos asignados para esta urna. Si la desasignas, esos turnos se eliminarán. ¿Deseas continuar?" flash: destroy: "Urna desasignada" create: "Urna asignada" @@ -952,6 +958,7 @@ es: direct_messages: Mensajes directos proposal_notifications: Notificaciones de propuestas incomplete_verifications: Verificaciones incompletas + polls: Votaciones direct_messages: title: Mensajes directos total: Total @@ -960,6 +967,17 @@ es: title: Notificaciones de propuestas total: Total proposals_with_notifications: Propuestas con notificaciones + polls: + title: Estadísticas de votaciones + all: Votaciones + web_participants: Participantes en Web + total_participants: Participantes totales + poll_questions: "Preguntas de votación: %{poll}" + table: + poll_name: Votación + question_name: Pregunta + origin_web: Participantes Web + origin_total: Participantes Totales tags: create: Crear tema destroy: Eliminar tema diff --git a/config/locales/es/settings.yml b/config/locales/es/settings.yml index fcf2eb1db..ae31d0a4a 100644 --- a/config/locales/es/settings.yml +++ b/config/locales/es/settings.yml @@ -48,6 +48,7 @@ es: map_zoom: Zoom mailer_from_name: Nombre email remitente mailer_from_address: Dirección email remitente + meta_title: "Título del sitio (SEO)" meta_description: "Descripción del sitio (SEO)" meta_keywords: "Palabras clave (SEO)" verification_offices_url: URL oficinas verificación diff --git a/config/routes.rb b/config/routes.rb index 491127439..31d811293 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,6 +36,8 @@ Rails.application.routes.draw do get '/welcome', to: 'welcome#welcome' get '/cuentasegura', to: 'welcome#verification', as: :cuentasegura + get '/consul.json', to: "installation#details" + resources :debates do member do post :vote @@ -272,7 +274,7 @@ Rails.application.routes.draw do get :search, on: :collection end - resources :valuators, only: [:index, :create] do + resources :valuators, only: [:index, :create, :destroy] do get :search, on: :collection get :summary, on: :collection end @@ -339,6 +341,7 @@ Rails.application.routes.draw do resource :stats, only: :show do get :proposal_notifications, on: :collection get :direct_messages, on: :collection + get :polls, on: :collection end namespace :legislation do diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 60493e0a5..8476f7693 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -50,11 +50,13 @@ section "Creating Settings" do Setting.create(key: 'feature.user.recommendations', value: "true") Setting.create(key: 'feature.community', value: "true") Setting.create(key: 'feature.map', value: "true") + Setting.create(key: 'feature.public_stats', value: "true") Setting.create(key: 'per_page_code_head', value: "") Setting.create(key: 'per_page_code_body', value: "") Setting.create(key: 'comments_body_max_length', value: '1000') Setting.create(key: 'mailer_from_name', value: 'CONSUL') Setting.create(key: 'mailer_from_address', value: 'noreply@consul.dev') + Setting.create(key: 'meta_title', value: 'CONSUL') Setting.create(key: 'meta_description', value: 'Citizen Participation and Open Government Application') Setting.create(key: 'meta_keywords', value: 'citizen participation, open government') Setting.create(key: 'verification_offices_url', value: 'http://oficinas-atencion-ciudadano.url/') @@ -63,6 +65,7 @@ section "Creating Settings" do Setting.create(key: 'map_latitude', value: 51.48) Setting.create(key: 'map_longitude', value: 0.0) Setting.create(key: 'map_zoom', value: 10) + Setting.create(key: 'related_contents_report_threshold', value: 2) end section "Creating Geozones" do diff --git a/db/migrate/20171127171925_create_related_content.rb b/db/migrate/20171127171925_create_related_content.rb new file mode 100644 index 000000000..2944938e2 --- /dev/null +++ b/db/migrate/20171127171925_create_related_content.rb @@ -0,0 +1,12 @@ +class CreateRelatedContent < ActiveRecord::Migration + def change + create_table :related_contents do |t| + t.references :parent_relationable, polymorphic: true, index: { name: 'index_related_contents_on_parent_relationable' } + t.references :child_relationable, polymorphic: true, index: { name: 'index_related_contents_on_child_relationable' } + t.references :related_content, index: { name: 'opposite_related_content' } + t.timestamps + end + + add_index :related_contents, [:parent_relationable_id, :parent_relationable_type, :child_relationable_id, :child_relationable_type], name: "unique_parent_child_related_content", unique: true, using: :btree + end +end diff --git a/db/migrate/20171127230716_add_time_reported_to_related_content.rb b/db/migrate/20171127230716_add_time_reported_to_related_content.rb new file mode 100644 index 000000000..5cca12cf3 --- /dev/null +++ b/db/migrate/20171127230716_add_time_reported_to_related_content.rb @@ -0,0 +1,5 @@ +class AddTimeReportedToRelatedContent < ActiveRecord::Migration + def change + add_column :related_contents, :times_reported, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index f2dd62428..4d426cf9e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171115164152) do +ActiveRecord::Schema.define(version: 20171127230716) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -851,6 +851,22 @@ ActiveRecord::Schema.define(version: 20171115164152) do add_index "proposals", ["title"], name: "index_proposals_on_title", using: :btree add_index "proposals", ["tsv"], name: "index_proposals_on_tsv", using: :gin + create_table "related_contents", force: :cascade do |t| + t.integer "parent_relationable_id" + t.string "parent_relationable_type" + t.integer "child_relationable_id" + t.string "child_relationable_type" + t.integer "related_content_id" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "times_reported", default: 0 + end + + add_index "related_contents", ["child_relationable_type", "child_relationable_id"], name: "index_related_contents_on_child_relationable", using: :btree + add_index "related_contents", ["parent_relationable_id", "parent_relationable_type", "child_relationable_id", "child_relationable_type"], name: "unique_parent_child_related_content", unique: true, using: :btree + add_index "related_contents", ["parent_relationable_type", "parent_relationable_id"], name: "index_related_contents_on_parent_relationable", using: :btree + add_index "related_contents", ["related_content_id"], name: "opposite_related_content", using: :btree + create_table "settings", force: :cascade do |t| t.string "key" t.string "value" diff --git a/db/seeds.rb b/db/seeds.rb index fcfa6340c..6ef55067c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -65,6 +65,7 @@ Setting["org_name"] = "CONSUL" Setting["place_name"] = "CONSUL-land" # Meta tags for SEO +Setting["meta_title"] = nil Setting["meta_description"] = nil Setting["meta_keywords"] = nil @@ -115,3 +116,6 @@ Setting['proposal_improvement_path'] = nil Setting['map_latitude'] = 51.48 Setting['map_longitude'] = 0.0 Setting['map_zoom'] = 10 + +# Related content +Setting['related_contents_report_threshold'] = 5 diff --git a/spec/controllers/installation_controller_spec.rb b/spec/controllers/installation_controller_spec.rb new file mode 100644 index 000000000..68c2e7c07 --- /dev/null +++ b/spec/controllers/installation_controller_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe InstallationController, type: :request do + + describe "consul.json" do + let(:feature_settings) do + { + 'debates' => nil, + 'spending_proposals' => 't', + 'polls' => nil, + 'twitter_login' => nil, + 'facebook_login' => nil, + 'google_login' => nil, + 'public_stats' => nil, + 'budgets' => nil, + 'signature_sheets' => nil, + 'legislation' => nil, + 'user.recommendations' => nil, + 'community' => nil, + 'map' => 't', + 'spending_proposal_features.voting_allowed' => 't' + } + end + + before do + feature_settings.each { |feature_name, feature_value| Setting["feature.#{feature_name}"] = feature_value } + end + + after do + Setting['feature.debates'] = true + Setting['feature.spending_proposals'] = nil + Setting['feature.polls'] = true + Setting['feature.twitter_login'] = true + Setting['feature.facebook_login'] = true + Setting['feature.google_login'] = true + Setting['feature.public_stats'] = true + Setting['feature.budgets'] = true + Setting['feature.signature_sheets'] = true + Setting['feature.legislation'] = true + Setting['feature.user.recommendations'] = true + Setting['feature.community'] = true + Setting['feature.map'] = nil + Setting['feature.spending_proposal_features.voting_allowed'] = nil + end + + specify "with query string inside query params" do + get '/consul.json' + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['release']).not_to be_empty + expect(JSON.parse(response.body)['features']).to eq(feature_settings) + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index b98fc7adb..1ac2f3ebd 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -882,4 +882,7 @@ LOREM_IPSUM end end + factory :related_content do + end + end diff --git a/spec/features/admin/budget_investments_spec.rb b/spec/features/admin/budget_investments_spec.rb index 7252e2660..1ad89e6aa 100644 --- a/spec/features/admin/budget_investments_spec.rb +++ b/spec/features/admin/budget_investments_spec.rb @@ -421,20 +421,37 @@ feature 'Admin budget investments' do end end - scenario "Only displays valuation tags" do + scenario "Changes valuation and user generated tags" do budget_investment = create(:budget_investment, tag_list: 'Park') budget_investment.set_tag_list_on(:valuation, 'Education') budget_investment.save visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment) - expect(page).to have_content "Education" - expect(page).to_not have_content "Park" + within("#user-tags") do + expect(page).to_not have_content "Education" + expect(page).to have_content "Park" + end click_link 'Edit classification' - expect(page).to have_content "Education" - expect(page).to_not have_content "Park" + fill_in 'budget_investment_tag_list', with: 'Park, Trees' + fill_in 'budget_investment_valuation_tag_list', with: 'Education, Environment' + click_button 'Update' + + visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment) + + within("#user-tags") do + expect(page).to_not have_content "Education" + expect(page).to_not have_content "Environment" + expect(page).to have_content "Park, Trees" + end + + within("#tags") do + expect(page).to have_content "Education, Environment" + expect(page).to_not have_content "Park" + expect(page).to_not have_content "Trees" + end end scenario "Maintains user tags" do diff --git a/spec/features/admin/poll/booth_assigments_spec.rb b/spec/features/admin/poll/booth_assigments_spec.rb index cceec1a3e..29e1eccde 100644 --- a/spec/features/admin/poll/booth_assigments_spec.rb +++ b/spec/features/admin/poll/booth_assigments_spec.rb @@ -106,6 +106,41 @@ feature 'Admin booths assignments' do expect(page).to have_content 'There are no booths assigned to this poll.' expect(page).not_to have_content booth.name end + + scenario 'Unassing booth whith associated shifts', :js do + assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + officer = create(:poll_officer) + create(:poll_officer_assignment, officer: officer, booth_assignment: assignment) + create(:poll_shift, booth: booth, officer: officer) + + visit manage_admin_poll_booth_assignments_path(poll) + + within("#poll_booth_#{booth.id}") do + expect(page).to have_content(booth.name) + expect(page).to have_content "Assigned" + + click_link 'Unassign booth' + + expect(page).to have_content "Unassigned" + expect(page).not_to have_content "Assigned" + expect(page).to have_link "Assign booth" + end + end + + scenario "Cannot unassing booth if poll is expired" do + poll_expired = create(:poll, :expired) + create(:poll_booth_assignment, poll: poll_expired, booth: booth) + + visit manage_admin_poll_booth_assignments_path(poll_expired) + + within("#poll_booth_#{booth.id}") do + expect(page).to have_content(booth.name) + expect(page).to have_content "Assigned" + + expect(page).not_to have_link 'Unassign booth' + end + + end end feature 'Show' do diff --git a/spec/features/admin/stats_spec.rb b/spec/features/admin/stats_spec.rb index c0aefe4e5..60dae016b 100644 --- a/spec/features/admin/stats_spec.rb +++ b/spec/features/admin/stats_spec.rb @@ -162,4 +162,99 @@ feature 'Stats' do end + context "Polls" do + + scenario "Total participants by origin" do + oa = create(:poll_officer_assignment) + 3.times { create(:poll_voter, origin: "web") } + + visit admin_stats_path + + within(".stats") do + click_link "Polls" + end + + within("#web_participants") do + expect(page).to have_content "3" + end + end + + scenario "Total participants" do + user = create(:user, :level_two) + 3.times { create(:poll_voter, user: user) } + create(:poll_voter) + + visit admin_stats_path + + within(".stats") do + click_link "Polls" + end + + within("#participants") do + expect(page).to have_content "2" + end + end + + scenario "Participants by poll" do + oa = create(:poll_officer_assignment) + + poll1 = create(:poll) + poll2 = create(:poll) + + 1.times { create(:poll_voter, poll: poll1, origin: "web") } + 2.times { create(:poll_voter, poll: poll2, origin: "web") } + + visit admin_stats_path + + within(".stats") do + click_link "Polls" + end + + within("#polls") do + + within("#poll_#{poll1.id}") do + expect(page).to have_content "1" + end + + within("#poll_#{poll2.id}") do + expect(page).to have_content "2" + end + + end + end + + scenario "Participants by poll question" do + user1 = create(:user, :level_two) + user2 = create(:user, :level_two) + + poll = create(:poll) + + question1 = create(:poll_question, :with_answers, poll: poll) + question2 = create(:poll_question, :with_answers, poll: poll) + + create(:poll_answer, question: question1, author: user1) + create(:poll_answer, question: question2, author: user1) + create(:poll_answer, question: question2, author: user2) + + visit admin_stats_path + + within(".stats") do + click_link "Polls" + end + + within("#poll_question_#{question1.id}") do + expect(page).to have_content "1" + end + + within("#poll_question_#{question2.id}") do + expect(page).to have_content "2" + end + + within("#poll_#{poll.id}_questions_total") do + expect(page).to have_content "2" + end + end + + end + end diff --git a/spec/features/admin/valuators_spec.rb b/spec/features/admin/valuators_spec.rb index bb631cbc3..f1dfa68b3 100644 --- a/spec/features/admin/valuators_spec.rb +++ b/spec/features/admin/valuators_spec.rb @@ -29,6 +29,14 @@ feature 'Admin valuators' do end end + scenario 'Delete Valuator' do + click_link 'Delete' + + within('#valuators') do + expect(page).to_not have_content(@valuator.name) + end + end + context 'Search' do background do diff --git a/spec/features/budgets/investments_spec.rb b/spec/features/budgets/investments_spec.rb index d17bd9c91..fa7749a4b 100644 --- a/spec/features/budgets/investments_spec.rb +++ b/spec/features/budgets/investments_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require 'sessions_helper' feature 'Budget Investments' do @@ -170,6 +171,25 @@ feature 'Budget Investments' do expect(order).to eq(new_order) end + scenario "Investments are not repeated with random order", :js do + 12.times { create(:budget_investment, heading: heading) } + # 12 instead of per_page + 2 because in each page there are 10 (in this case), not 25 + + visit budget_investments_path(budget, order: 'random') + + first_page_investments = investments_order + + click_link 'Next' + expect(page).to have_content "You're on page 2" + + second_page_investments = investments_order + + common_values = first_page_investments & second_page_investments + + expect(common_values.length).to eq(0) + + end + scenario 'Proposals are ordered by confidence_score', :js do create(:budget_investment, heading: heading, title: 'Best proposal').update_column(:confidence_score, 10) create(:budget_investment, heading: heading, title: 'Worst proposal').update_column(:confidence_score, 2) @@ -188,6 +208,46 @@ feature 'Budget Investments' do expect(current_url).to include('page=1') end + scenario 'Each user as a different and consistent random budget investment order', :js do + 12.times { create(:budget_investment, heading: heading) } + + in_browser(:one) do + visit budget_investments_path(budget, heading: heading) + @first_user_investments_order = investments_order + end + + in_browser(:two) do + visit budget_investments_path(budget, heading: heading) + @second_user_investments_order = investments_order + end + + expect(@first_user_investments_order).not_to eq(@second_user_investments_order) + + in_browser(:one) do + click_link 'Next' + expect(page).to have_content "You're on page 2" + + click_link 'Previous' + expect(page).to have_content "You're on page 1" + + expect(investments_order).to eq(@first_user_investments_order) + end + + in_browser(:two) do + click_link 'Next' + expect(page).to have_content "You're on page 2" + + click_link 'Previous' + expect(page).to have_content "You're on page 1" + + expect(investments_order).to eq(@second_user_investments_order) + end + end + + def investments_order + all(".budget-investment h3").collect {|i| i.text } + end + end context 'Phase I - Accepting' do diff --git a/spec/features/proposals_spec.rb b/spec/features/proposals_spec.rb index 81f7fb998..192e7949f 100644 --- a/spec/features/proposals_spec.rb +++ b/spec/features/proposals_spec.rb @@ -38,7 +38,7 @@ feature 'Proposals' do within("ul.pagination") do expect(page).to have_content("1") - expect(page).to have_content("2") + expect(page).to have_link('2', href: 'http://www.example.com/proposals?page=2') expect(page).to_not have_content("3") click_link "Next", exact: false end @@ -153,8 +153,8 @@ feature 'Proposals' do proposal = create(:proposal) visit proposal_path(proposal) - expect(page.html).to include "" - expect(page.html).to include "" + expect(page).to have_css "meta[name='twitter:title'][content=\"#{proposal.title}\"]", visible: false + expect(page).to have_css "meta[property='og:title'][content=\"#{proposal.title}\"]", visible: false end scenario 'Create' do diff --git a/spec/features/social_media_meta_tags_spec.rb b/spec/features/social_media_meta_tags_spec.rb new file mode 100644 index 000000000..d04df78f6 --- /dev/null +++ b/spec/features/social_media_meta_tags_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +feature 'Social media meta tags' do + + context 'Setting social media meta tags' do + + let(:meta_keywords) { 'citizen, participation, open government' } + let(:meta_title) { 'CONSUL TEST' } + let(:meta_description) { 'Citizen Participation and Open Government Application' } + let(:twitter_handle) { '@consul_test' } + let(:url) { 'http://consul.dev' } + let(:facebook_handle) { 'consultest' } + let(:org_name) { 'CONSUL TEST' } + + before do + Setting['meta_keywords'] = meta_keywords + Setting['meta_title'] = meta_title + Setting['meta_description'] = meta_description + Setting['twitter_handle'] = twitter_handle + Setting['url'] = url + Setting['facebook_handle'] = facebook_handle + Setting['org_name'] = org_name + end + + after do + Setting['meta_keywords'] = nil + Setting['meta_title'] = nil + Setting['meta_description'] = nil + Setting['twitter_handle'] = nil + Setting['url'] = 'http://example.com' + Setting['facebook_handle'] = nil + Setting['org_name'] = 'CONSUL' + end + + scenario 'Social media meta tags partial render settings content' do + + visit root_path + + expect(page).to have_css 'meta[name="keywords"][content="'+ meta_keywords + '"]', visible: false + expect(page).to have_css 'meta[name="twitter:site"][content="'+ twitter_handle + '"]', visible: false + expect(page).to have_css 'meta[name="twitter:title"][content="'+ meta_title + '"]', visible: false + expect(page).to have_css 'meta[name="twitter:description"][content="' + meta_description + '"]', visible: false + expect(page).to have_css 'meta[name="twitter:image"][content="http://www.example.com/social_media_icon_twitter.png"]', visible: false + expect(page).to have_css 'meta[property="og:title"][content="'+ meta_title + '"]', visible: false + expect(page).to have_css 'meta[property="article:publisher"][content="' + url + '"]', visible: false + expect(page).to have_css 'meta[property="article:author"][content="https://www.facebook.com/' + facebook_handle + '"]', visible: false + expect(page).to have_css 'meta[property="og:url"][content="http://www.example.com/"]', visible: false + expect(page).to have_css 'meta[property="og:image"][content="http://www.example.com/social_media_icon.png"]', visible: false + expect(page).to have_css 'meta[property="og:site_name"][content="' + org_name + '"]', visible: false + expect(page).to have_css 'meta[property="og:description"][content="' + meta_description + '"]', visible: false + end + end + +end diff --git a/spec/lib/graphql_spec.rb b/spec/lib/graphql_spec.rb index d6ea071e2..4f03e0f6a 100644 --- a/spec/lib/graphql_spec.rb +++ b/spec/lib/graphql_spec.rb @@ -282,15 +282,16 @@ describe 'ConsulSchema' do end describe 'Comments' do - it 'only returns comments from proposals and debates' do + it 'only returns comments from proposals, debates and polls' do proposal_comment = create(:comment, commentable: create(:proposal)) debate_comment = create(:comment, commentable: create(:debate)) + poll_comment = create(:comment, commentable: create(:poll)) spending_proposal_comment = build(:comment, commentable: create(:spending_proposal)).save(skip_validation: true) response = execute('{ comments { edges { node { commentable_type } } } }') received_commentables = extract_fields(response, 'comments', 'commentable_type') - expect(received_commentables).to match_array ['Proposal', 'Debate'] + expect(received_commentables).to match_array ['Proposal', 'Debate', 'Poll'] end it 'displays comments of authors even if public activity is set to false' do @@ -355,6 +356,19 @@ describe 'ConsulSchema' do expect(received_comments).to match_array [visible_debate_comment.body] end + it 'does not include comments from hidden polls' do + visible_poll = create(:poll) + hidden_poll = create(:poll, hidden_at: Time.current) + + visible_poll_comment = create(:comment, commentable: visible_poll) + hidden_poll_comment = create(:comment, commentable: hidden_poll) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to match_array [visible_poll_comment.body] + end + it 'does not include comments of debates that are not public' do not_public_debate = create(:debate, :hidden) not_public_debate_comment = create(:comment, commentable: not_public_debate) @@ -377,6 +391,17 @@ describe 'ConsulSchema' do expect(received_comments).to_not include(not_public_proposal_comment.body) end + it 'does not include comments of polls that are not public' do + not_public_poll = create(:poll) + not_public_poll_comment = create(:comment, commentable: not_public_poll) + allow(Comment).to receive(:public_for_api).and_return([]) + + response = execute('{ comments { edges { node { body } } } }') + received_comments = extract_fields(response, 'comments', 'body') + + expect(received_comments).to_not include(not_public_poll_comment.body) + end + it 'only returns date and hour for created_at' do created_at = Time.zone.parse("2017-12-31 9:30:15") create(:comment, created_at: created_at) diff --git a/spec/models/budget/heading_spec.rb b/spec/models/budget/heading_spec.rb index ee2e4c142..76df1fa5c 100644 --- a/spec/models/budget/heading_spec.rb +++ b/spec/models/budget/heading_spec.rb @@ -25,4 +25,23 @@ describe Budget::Heading do end end + describe "Save population" do + it "Allows population == nil" do + expect(create(:budget_heading, group: group, name: 'Population is nil', population: nil)).to be_valid + end + + it "Doesn't allow population <= 0" do + heading = create(:budget_heading, group: group, name: 'Population is > 0') + + heading.population = 0 + expect(heading).not_to be_valid + + heading.population = -10 + expect(heading).not_to be_valid + + heading.population = 10 + expect(heading).to be_valid + end + end + end diff --git a/spec/models/poll/booth_assignment_spec.rb b/spec/models/poll/booth_assignment_spec.rb new file mode 100644 index 000000000..8663e3872 --- /dev/null +++ b/spec/models/poll/booth_assignment_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe :booth_assignment do + let(:poll){create(:poll)} + let(:booth){create(:poll_booth)} + let(:booth1){create(:poll_booth)} + + it "should check if there are shifts" do + assignment_with_shifts = create(:poll_booth_assignment, poll: poll, booth: booth) + assignment_without_shifts = create(:poll_booth_assignment, poll: poll, booth: booth1) + officer = create(:poll_officer) + create(:poll_officer_assignment, officer: officer, booth_assignment: assignment_with_shifts) + create(:poll_shift, booth: booth, officer: officer) + + expect(assignment_with_shifts.shifts?).to eq(true) + expect(assignment_without_shifts.shifts?).to eq(false) + end + + it "should delete shifts associated to booth assignments" do + assignment = create(:poll_booth_assignment, poll: poll, booth: booth) + officer = create(:poll_officer) + create(:poll_officer_assignment, officer: officer, booth_assignment: assignment) + create(:poll_shift, booth: booth, officer: officer) + + assignment.destroy + + expect(Poll::Shift.all.count).to eq(0) + end +end \ No newline at end of file diff --git a/spec/models/relation_spec.rb b/spec/models/relation_spec.rb new file mode 100644 index 000000000..86bbe2c14 --- /dev/null +++ b/spec/models/relation_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +describe RelatedContent do + + let(:parent_relationable) { create([:proposal, :debate, :budget_investment].sample) } + let(:child_relationable) { create([:proposal, :debate, :budget_investment].sample) } + + it "should allow relationables from various classes" do + expect(build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)).to be_valid + expect(build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)).to be_valid + expect(build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable)).to be_valid + end + + it "should not allow empty relationables" do + expect(build(:related_content)).not_to be_valid + expect(build(:related_content, parent_relationable: parent_relationable)).not_to be_valid + expect(build(:related_content, child_relationable: child_relationable)).not_to be_valid + end + + it "should not allow repeated related contents" do + related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable) + new_related_content = build(:related_content, parent_relationable: related_content.parent_relationable, child_relationable: related_content.child_relationable) + expect(new_related_content).not_to be_valid + end + + describe 'create_opposite_related_content' do + let(:parent_relationable) { create(:proposal) } + let(:child_relationable) { create(:debate) } + let(:related_content) { build(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable) } + + it 'creates an opposite related_content' do + expect { related_content.save }.to change { RelatedContent.count }.by(2) + expect(related_content.opposite_related_content.child_relationable_id).to eq(parent_relationable.id) + expect(related_content.opposite_related_content.child_relationable_type).to eq(parent_relationable.class.name) + expect(related_content.opposite_related_content.parent_relationable_id).to eq(child_relationable.id) + expect(related_content.opposite_related_content.parent_relationable_type).to eq(child_relationable.class.name) + expect(related_content.opposite_related_content.opposite_related_content.id).to eq(related_content.id) + end + end + + describe 'relationable destroy' do + let(:parent_relationable) { create(:proposal) } + let(:child_relationable) { create(:debate) } + + it 'destroys both related contents involved' do + related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable) + expect { related_content.parent_relationable.destroy }.to change { RelatedContent.all.count }.by(-2) + expect(child_relationable.related_contents).to be_empty + end + end + + # TODO: Move this into a Relationable shared context + describe '#report_related_content' do + it 'increments both relation and opposite relation times_reported counters' do + related_content = create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable) + parent_relationable.report_related_content(child_relationable) + + expect(related_content.reload.times_reported).to eq(1) + expect(related_content.reload.opposite_related_content.times_reported).to eq(1) + end + end + + describe '#relationed_contents' do + before do + create(:related_content, parent_relationable: parent_relationable, child_relationable: create(:proposal), times_reported: 6) + create(:related_content, parent_relationable: parent_relationable, child_relationable: child_relationable) + end + + it 'returns not hidden by reports related contents' do + expect(parent_relationable.relationed_contents.count).to eq(1) + expect(parent_relationable.relationed_contents.first.class.name).to eq(child_relationable.class.name) + expect(parent_relationable.relationed_contents.first.id).to eq(child_relationable.id) + end + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a0e4a98b0..7beab6ff1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,8 @@ require 'factory_girl_rails' require 'database_cleaner' require 'email_spec' require 'devise' -require 'knapsack' +require 'knapsack_pro' + Dir["./spec/models/concerns/*.rb"].each { |f| require f } Dir["./spec/support/**/*.rb"].sort.each { |f| require f } Dir["./spec/shared/**/*.rb"].sort.each { |f| require f } @@ -107,4 +108,4 @@ RSpec.configure do |config| end # Parallel build helper configuration for travis -Knapsack::Adapters::RSpecAdapter.bind +KnapsackPro::Adapters::RSpecAdapter.bind