Merge branch 'master' into aperez-email-specs-for-commentables

This commit is contained in:
Raimond Garcia
2017-12-07 13:05:25 +01:00
committed by GitHub
68 changed files with 1084 additions and 121 deletions

View File

@@ -10,10 +10,10 @@ before_script:
- bundle exec rake db:setup - bundle exec rake db:setup
script: script:
- "bundle exec rake assets:precompile RAILS_ENV=test" - "bundle exec rake assets:precompile RAILS_ENV=test"
- "bundle exec rake knapsack:rspec" - "bin/knapsack_pro_rspec"
env: env:
global: global:
- CI_NODE_TOTAL=2 - KNAPSACK_PRO_CI_NODE_TOTAL=2
matrix: matrix:
- CI_NODE_INDEX=0 - KNAPSACK_PRO_CI_NODE_INDEX=0
- CI_NODE_INDEX=1 - KNAPSACK_PRO_CI_NODE_INDEX=1

View File

@@ -1,38 +1,148 @@
### 0.9 - 2017-06-15 # Changelog
All notable changes to this project will be documented in this file.
* New features The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
* Budgets and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
* 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
### 0.8 - 2016-07-21 ## [Unreleased](https://github.com/consul/consul/compare/v0.11...consul:master)
* New features ## [0.11.0](https://github.com/consul/consul/compare/v0.10...v0.11) - 2017-12-05
* 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.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 ### Changed
* Debates - Gem versions locked & cleanup https://github.com/consul/consul/pull/1730
* Proposals - Upgraded many minor versions https://github.com/consul/consul/pull/1747
* Basic Spending Proposals - Rails 4.2.10 https://github.com/consul/consul/pull/2128
* Enhancements - Updated Code of Conduct to use contributor covenant 1.4 https://github.com/consul/consul/pull/1733
* Rails 2.4.6 - Improved consistency to all "Go back" buttons https://github.com/consul/consul/pull/1770
* Ruby 2.2.3 - 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

View File

@@ -64,7 +64,7 @@ group :development, :test do
gem 'factory_girl_rails', '~> 4.8.0' gem 'factory_girl_rails', '~> 4.8.0'
gem 'faker', '~> 1.7.3' gem 'faker', '~> 1.7.3'
gem 'i18n-tasks', '~> 0.9.15' gem 'i18n-tasks', '~> 0.9.15'
gem 'knapsack', '~> 1.13.3' gem 'knapsack_pro', '~> 0.53.0'
gem 'launchy', '~> 2.4.3' gem 'launchy', '~> 2.4.3'
gem 'letter_opener_web', '~> 1.3.1' gem 'letter_opener_web', '~> 1.3.1'
gem 'quiet_assets', '~> 1.1.0' gem 'quiet_assets', '~> 1.1.0'

View File

@@ -229,9 +229,8 @@ GEM
kaminari-core (= 1.0.1) kaminari-core (= 1.0.1)
kaminari-core (1.0.1) kaminari-core (1.0.1)
kgio (2.11.0) kgio (2.11.0)
knapsack (1.13.3) knapsack_pro (0.53.0)
rake rake
timecop (>= 0.1.0)
kramdown (1.14.0) kramdown (1.14.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
@@ -451,7 +450,6 @@ GEM
thread (0.2.2) thread (0.2.2)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.7) tilt (2.0.7)
timecop (0.9.1)
tins (1.15.0) tins (1.15.0)
turbolinks (2.5.3) turbolinks (2.5.3)
coffee-rails coffee-rails
@@ -531,7 +529,7 @@ DEPENDENCIES
jquery-rails (~> 4.3.1) jquery-rails (~> 4.3.1)
jquery-ui-rails (~> 6.0.1) jquery-ui-rails (~> 6.0.1)
kaminari (~> 1.0.1) kaminari (~> 1.0.1)
knapsack (~> 1.13.3) knapsack_pro (~> 0.53.0)
launchy (~> 2.4.3) launchy (~> 2.4.3)
letter_opener_web (~> 1.3.1) letter_opener_web (~> 1.3.1)
mdl (~> 0.4.0) mdl (~> 0.4.0)
@@ -573,4 +571,4 @@ DEPENDENCIES
whenever (~> 0.9.7) whenever (~> 0.9.7)
BUNDLED WITH BUNDLED WITH
1.15.4 1.16.0

View File

@@ -4,4 +4,4 @@
require File.expand_path('../config/application', __FILE__) require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks Rails.application.load_tasks
Knapsack.load_tasks if defined?(Knapsack) KnapsackPro.load_tasks if defined?(KnapsackPro)

View File

@@ -73,6 +73,7 @@
//= require map //= require map
//= require polls //= require polls
//= require sortable //= require sortable
//= require table_sortable
var initialize_modules = function() { var initialize_modules = function() {
App.Comments.initialize(); App.Comments.initialize();
@@ -113,6 +114,7 @@ var initialize_modules = function() {
App.Map.initialize(); App.Map.initialize();
App.Polls.initialize(); App.Polls.initialize();
App.Sortable.initialize(); App.Sortable.initialize();
App.TableSortable.initialize();
}; };
$(function(){ $(function(){

View File

@@ -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

View File

@@ -270,6 +270,11 @@ $sidebar-active: #f4fcd0;
} }
} }
.sortable thead th:hover {
text-decoration: underline;
cursor: pointer;
}
// 02. Sidebar // 02. Sidebar
// ----------- // -----------

View File

@@ -51,7 +51,7 @@ class Admin::BudgetInvestmentsController < Admin::BaseController
def budget_investment_params def budget_investment_params
params.require(:budget_investment) 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: []) :selected, valuator_ids: [])
end end

View File

@@ -36,4 +36,9 @@ class Admin::StatsController < Admin::BaseController
@users_who_have_sent_message = DirectMessage.select(:sender_id).distinct.count @users_who_have_sent_message = DirectMessage.select(:sender_id).distinct.count
end end
def polls
@polls = ::Poll.current
@participants = ::Poll::Voter.where(poll: @polls)
end
end end

View File

@@ -19,6 +19,11 @@ class Admin::ValuatorsController < Admin::BaseController
redirect_to admin_valuators_path redirect_to admin_valuators_path
end end
def destroy
@valuator.destroy
redirect_to admin_valuators_path
end
def summary def summary
@valuators = Valuator.order(spending_proposals_count: :desc) @valuators = Valuator.order(spending_proposals_count: :desc)
end end

View File

@@ -28,8 +28,8 @@ module Budgets
respond_to :html, :js respond_to :html, :js
def index def index
@investments = @investments.apply_filters_and_search(@budget, params, @current_filter) @investments = investments.page(params[:page]).per(10).for_render
.send("sort_by_#{@current_order}").page(params[:page]).per(10).for_render
@investment_ids = @investments.pluck(:id) @investment_ids = @investments.pluck(:id)
load_investment_votes(@investments) load_investment_votes(@investments)
@tag_cloud = tag_cloud @tag_cloud = tag_cloud
@@ -94,9 +94,8 @@ module Budgets
def set_random_seed def set_random_seed
if params[:order] == 'random' || params[:order].blank? if params[:order] == 'random' || params[:order].blank?
params[:random_seed] ||= rand(99) / 100.0 seed = rand(-100..100) / 100.0
seed = Float(params[:random_seed]) rescue 0 params[:random_seed] ||= Float(seed) rescue 0
Budget::Investment.connection.execute("select setseed(#{seed})")
else else
params[:random_seed] = nil params[:random_seed] = nil
end end
@@ -131,6 +130,16 @@ module Budgets
TagCloud.new(Budget::Investment, params[:search]) TagCloud.new(Budget::Investment, params[:search])
end 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
end end

View File

@@ -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

View File

@@ -60,4 +60,8 @@ module ApplicationHelper
def format_price(number) def format_price(number)
number_to_currency(number, precision: 0, locale: I18n.default_locale) number_to_currency(number, precision: 0, locale: I18n.default_locale)
end end
def kaminari_path(url)
"#{root_url.chomp("\/")}#{url}"
end
end end

View File

@@ -43,7 +43,7 @@ module Abilities
can [:search, :create, :index, :destroy], ::Administrator can [:search, :create, :index, :destroy], ::Administrator
can [:search, :create, :index, :destroy], ::Moderator 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, :create, :index, :destroy], ::Manager
can [:search, :index], ::User can [:search, :index], ::User

View File

@@ -10,6 +10,7 @@ class Budget
validates :name, presence: true, uniqueness: { if: :name_exists_in_budget_headings } validates :name, presence: true, uniqueness: { if: :name_exists_in_budget_headings }
validates :price, presence: true validates :price, presence: true
validates :slug, presence: true, format: /\A[a-z0-9\-_]+\z/ 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 delegate :budget, :budget_id, to: :group, allow_nil: true

View File

@@ -17,6 +17,7 @@ class Budget
acts_as_votable acts_as_votable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at
include ActsAsParanoidAliases include ActsAsParanoidAliases
include Relationable
belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
belongs_to :heading belongs_to :heading
@@ -43,7 +44,7 @@ class Budget
scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc, id: :desc) } 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_ballots, -> { reorder(ballot_lines_count: :desc, id: :desc) }
scope :sort_by_price, -> { reorder(price: :desc, confidence_score: :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 :valuation_open, -> { where(valuation_finished: false) }
scope :without_admin, -> { valuation_open.where(administrator_id: nil) } scope :without_admin, -> { valuation_open.where(administrator_id: nil) }

View File

@@ -30,9 +30,11 @@ class Comment < ActiveRecord::Base
scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) }
scope :public_for_api, -> do scope :public_for_api, -> do
where(%{(comments.commentable_type = 'Debate' and comments.commentable_id in (?)) or 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), 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 end
scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) } scope :sort_by_most_voted, -> { order(confidence_score: :desc, created_at: :desc) }

View File

@@ -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

View File

@@ -9,6 +9,7 @@ class Debate < ActiveRecord::Base
include Filterable include Filterable
include HasPublicAuthor include HasPublicAuthor
include Graphqlable include Graphqlable
include Relationable
acts_as_votable acts_as_votable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at

View File

@@ -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 :recounting, -> { Poll.where(ends_at: (Date.current.beginning_of_day - RECOUNT_DURATION)..Date.current.beginning_of_day) }
scope :published, -> { where('published = ?', true) } scope :published, -> { where('published = ?', true) }
scope :by_geozone_id, ->(geozone_id) { where(geozones: {id: geozone_id}.joins(:geozones)) } 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) } scope :sort_for_list, -> { order(:geozone_restricted, :starts_at, :name) }

View File

@@ -3,10 +3,26 @@ class Poll
belongs_to :booth belongs_to :booth
belongs_to :poll belongs_to :poll
before_destroy :destroy_poll_shifts, only: :destroy
has_many :officer_assignments, class_name: "Poll::OfficerAssignment", dependent: :destroy has_many :officer_assignments, class_name: "Poll::OfficerAssignment", dependent: :destroy
has_many :officers, through: :officer_assignments has_many :officers, through: :officer_assignments
has_many :voters has_many :voters
has_many :partial_results has_many :partial_results
has_many :recounts 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
end end

View File

@@ -17,6 +17,7 @@ class Proposal < ActiveRecord::Base
max_file_size: 3.megabytes, max_file_size: 3.megabytes,
accepted_content_types: [ "application/pdf" ] accepted_content_types: [ "application/pdf" ]
include EmbedVideosHelper include EmbedVideosHelper
include Relationable
acts_as_votable acts_as_votable
acts_as_paranoid column: :hidden_at acts_as_paranoid column: :hidden_at

View File

@@ -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

View File

@@ -56,3 +56,8 @@
<% end %> <% end %>
<%= safe_html_with_links @investment.description %> <%= safe_html_with_links @investment.description %>
<p id="user-tags">
<strong><%= t("admin.budget_investments.show.user_tags") %>: </strong>
<%= @investment.tag_list.sort.join(', ') %>
</p>

View File

@@ -18,6 +18,13 @@
<%= f.cktext_area :description, maxlength: Budget::Investment.description_max_length, ckeditor: { language: I18n.locale } %> <%= f.cktext_area :description, maxlength: Budget::Investment.description_max_length, ckeditor: { language: I18n.locale } %>
</div> </div>
<div class="small-12 column">
<%= f.label :tag_list, t("admin.budget_investments.edit.user_tags") %>
<%= f.text_field :tag_list,
value: @investment.tag_list.sort.join(','),
label: false %>
</div>
<div class="small-12 column"> <div class="small-12 column">
<%= f.text_field :external_url %> <%= f.text_field :external_url %>
</div> </div>
@@ -39,7 +46,7 @@
<div class="small-12 column"> <div class="small-12 column">
<%= f.label :tag_list, t("admin.budget_investments.edit.tags") %> <%= f.label :valuation_tag_list, t("admin.budget_investments.edit.tags") %>
<div class="tags"> <div class="tags">
<% @tags.each do |tag| %> <% @tags.each do |tag| %>
<a class="js-add-tag-link"><%= tag.name %></a> <a class="js-add-tag-link"><%= tag.name %></a>

View File

@@ -19,7 +19,7 @@
<p id="tags"> <p id="tags">
<strong><%= t("admin.budget_investments.show.tags") %>:</strong> <strong><%= t("admin.budget_investments.show.tags") %>:</strong>
<%= @investment.tags_on(:valuation).pluck(:name).join(', ') %> <%= @investment.tags_on(:valuation).pluck(:name).sort.join(', ') %>
</p> </p>
<p id="assigned_valuators"> <p id="assigned_valuators">

View File

@@ -49,10 +49,17 @@
<div class="row"> <div class="row">
<div class="small-12 medium-6 column"> <div class="small-12 medium-6 column">
<label><%= t("admin.budgets.form.population") %></label> <label><%= t("admin.budgets.form.population") %></label>
<%= f.text_field :population, <%= f.number_field :population,
label: false, label: false,
maxlength: 8, maxlength: 8,
placeholder: t("admin.budgets.form.population") %> min: 1,
placeholder: t("admin.budgets.form.population"),
data: {toggle_focus: "population-info"} %>
</div>
<div class="small-12 medium-6 column " >
<div id="population-info" class="is-hidden" data-toggler="is-hidden">
<%= t("admin.budgets.form.population_info") %>
</div>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,8 @@
method: :delete, method: :delete,
remote: true, remote: true,
title: t("admin.booth_assignments.manage.actions.unassign"), 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? %>
</td> </td>
<% else %> <% else %>
<td> <td>
@@ -26,6 +27,6 @@
method: :post, method: :post,
remote: true, remote: true,
title: t("admin.booth_assignments.manage.actions.assign"), title: t("admin.booth_assignments.manage.actions.assign"),
class: "button" %> class: "button" if !@poll.expired? %>
</td> </td>
<% end %> <% end %>

View File

@@ -0,0 +1,87 @@
<%= back_link_to %>
<h2 id="top"><%= t("admin.stats.polls.title")%></h2>
<div class="stats">
<div class="row stats-numbers">
<div class="small-12 medium-3 column">
<p class="featured">
<%= t("admin.stats.polls.web_participants") %><br>
<span id="web_participants" class="number">
<%= @participants.web.select(:user_id).distinct.count %>
</span>
</p>
</div>
<div class="small-12 medium-3 column end">
<p class="featured">
<%= t("admin.stats.polls.total_participants") %><br>
<span id="participants" class="number">
<%= @participants.select(:user_id).distinct.count %>
</span>
</p>
</div>
</div>
</div>
<h2><%= t("admin.stats.polls.all") %></h2>
<table id="polls" class="stack sortable">
<thead>
<tr>
<th><%= t("admin.stats.polls.table.poll_name") %></th>
<th class="name text-right"><%= t("admin.stats.polls.total_participants") %></th>
<th class="name text-right"><%= t("admin.stats.polls.table.origin_web") %></th>
</tr>
</thead>
<% @polls.each do |poll| %>
<tr id="<%= dom_id(poll) %>">
<td class="name">
<a href="#<%= dom_id(poll) %>_questions"><%= poll.name %></a>
</td>
<td class="name text-right">
<%= poll.voters.select(:user_id).distinct.count %>
</td>
<td class="name text-right">
<%= poll.voters.web.select(:user_id).distinct.count %>
</td>
</tr>
<% end %>
</table>
<% @polls.each do |poll| %>
<h3 id="<%= dom_id(poll) %>_questions">
<%= t("admin.stats.polls.poll_questions", poll: poll.name) %>
</h3>
<table class="stack sortable">
<thead>
<tr>
<th><%= t("admin.stats.polls.table.question_name") %></th>
<th class="name text-right">
<%= t("admin.stats.polls.table.origin_web") %>
</th>
</tr>
</thead>
<% poll.questions.each do |question| %>
<tr id="<%= dom_id(question) %>">
<td class="name">
<%= question.title %>
</td>
<td class="name text-right">
<%= ::Poll::Answer.by_question(question).count %>
</td>
</tr>
<% end %>
<tfoot>
<tr id="<%= dom_id(poll) %>_questions_total">
<th></th>
<th class="name text-right">
<strong>
<%= t("admin.stats.polls.table.origin_total") %>:
<%= ::Poll::Answer.where(question: poll.questions)
.select(:author_id).distinct.count %>
</strong>
</th>
</tr>
</tfoot>
</table>
<% end %>

View File

@@ -7,6 +7,8 @@
<h1 class="inline-block"><%= t "admin.stats.show.stats_title" %></h1> <h1 class="inline-block"><%= t "admin.stats.show.stats_title" %></h1>
<div class="float-right clear"> <div class="float-right clear">
<%= link_to t("admin.stats.show.polls"),
polls_admin_stats_path, class: "button hollow" %>
<%= link_to t("admin.stats.show.direct_messages"), <%= link_to t("admin.stats.show.direct_messages"),
direct_messages_admin_stats_path, class: "button hollow" %> direct_messages_admin_stats_path, class: "button hollow" %>
<%= link_to t("admin.stats.show.proposal_notifications"), <%= link_to t("admin.stats.show.proposal_notifications"),

View File

@@ -1,6 +1,6 @@
<h2><%= t("admin.valuators.index.title") %></h2> <h2><%= t("admin.valuators.index.title") %></h2>
<%= render 'admin/shared/user_search', url: search_admin_valuators_path %> <%= render "admin/shared/user_search", url: search_admin_valuators_path %>
<div id="valuators"> <div id="valuators">
<% if @valuators.any? %> <% if @valuators.any? %>
@@ -11,16 +11,13 @@
<th scope="col"><%= t("admin.valuators.index.name") %></th> <th scope="col"><%= t("admin.valuators.index.name") %></th>
<th scope="col"><%= t("admin.valuators.index.email") %></th> <th scope="col"><%= t("admin.valuators.index.email") %></th>
<th scope="col"><%= t("admin.valuators.index.description") %></th> <th scope="col"><%= t("admin.valuators.index.description") %></th>
<th scope="col"><%= t("admin.actions.actions") %></th>
</thead> </thead>
<tbody> <tbody>
<% @valuators.each do |valuator| %> <% @valuators.each do |valuator| %>
<tr> <tr>
<td> <td><%= valuator.name %></td>
<%= valuator.name %> <td><%= valuator.email %></td>
</td>
<td>
<%= valuator.email %>
</td>
<td> <td>
<% if valuator.description.present? %> <% if valuator.description.present? %>
<%= valuator.description %> <%= valuator.description %>
@@ -28,6 +25,12 @@
<%= t("admin.valuators.index.no_description") %> <%= t("admin.valuators.index.no_description") %>
<% end %> <% end %>
</td> </td>
<td>
<%= link_to t("admin.valuators.valuator.delete"),
admin_valuator_path(valuator),
method: :delete,
class: "button hollow alert expanded" %>
</td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>

View File

@@ -29,7 +29,12 @@
<%= t("admin.valuators.index.no_description") %> <%= t("admin.valuators.index.no_description") %>
<% end %> <% end %>
<td> <td>
<% 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| %> <%= form_for Valuator.new(user: user), url: admin_valuators_path do |f| %>
<%= f.text_field :description, <%= f.text_field :description,
label: false, label: false,

View File

@@ -1,3 +1,3 @@
<li> <li>
<%= link_to t("views.pagination.first").html_safe, url, :remote => remote %> <%= link_to t("views.pagination.first").html_safe, kaminari_path(url), :remote => remote %>
</li> </li>

View File

@@ -1,3 +1,3 @@
<li> <li>
<%= link_to t("views.pagination.last").html_safe, url, :remote => remote %> <%= link_to t("views.pagination.last").html_safe, kaminari_path(url), :remote => remote %>
</li> </li>

View File

@@ -1,3 +1,3 @@
<li class="pagination-next"> <li class="pagination-next">
<%= 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 %>
</li> </li>

View File

@@ -5,6 +5,6 @@
</li> </li>
<% else %> <% else %>
<li> <li>
<%= 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} %>
</li> </li>
<% end %> <% end %>

View File

@@ -1,3 +1,3 @@
<li class="pagination-previous"> <li class="pagination-previous">
<%= 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 %>
</li> </li>

View File

@@ -44,6 +44,7 @@
<% end %> <% end %>
</li> </li>
<% if Setting['feature.budgets'] %>
<li <%= "class=active" if (controller_name == "budget_investments" && action_name == "new") || <li <%= "class=active" if (controller_name == "budget_investments" && action_name == "new") ||
(controller_name == "budget" && action_name == 'create_investments') %>> (controller_name == "budget" && action_name == 'create_investments') %>>
<%= link_to create_investments_management_budgets_path do %> <%= link_to create_investments_management_budgets_path do %>
@@ -60,6 +61,15 @@
<% end %> <% end %>
</li> </li>
<li <%= "class=active" if (controller_name == "budget_investments" && action_name == "print") ||
(controller_name == "budgets" && action_name == "print_investments") %>>
<%= link_to print_investments_management_budgets_path do %>
<span class="icon-print"></span>
<%= t("management.menu.print_budget_investments") %>
<% end %>
</li>
<% end %>
<li <%= "class=active" if controller_name == "proposals" && action_name == "print" %>> <li <%= "class=active" if controller_name == "proposals" && action_name == "print" %>>
<%= link_to print_management_proposals_path do %> <%= link_to print_management_proposals_path do %>
<span class="icon-print"></span> <span class="icon-print"></span>
@@ -74,15 +84,6 @@
<% end %> <% end %>
</li> </li>
<li <%= "class=active" if (controller_name == "budget_investments" && action_name == "print") ||
(controller_name == "budgets" && action_name == "print_investments") %>>
<%= link_to print_investments_management_budgets_path do %>
<span class="icon-print"></span>
<%= t("management.menu.print_budget_investments") %>
<% end %>
</li>
<li> <li>
<%= link_to new_management_user_invite_path do %> <%= link_to new_management_user_invite_path do %>
<span class="icon-letter"></span> <span class="icon-letter"></span>

View File

@@ -1,20 +1,20 @@
<!-- Twitter --> <!-- Twitter -->
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary"/>
<meta name="twitter:site" content="@consul_dev" /> <meta name="twitter:site" content="<%= setting['twitter_handle'] %>"/>
<meta name="twitter:title" content="<%= social_title %>" /> <meta name="twitter:title" content="<%= local_assigns[:social_title] || setting['meta_title'] %>"/>
<meta name="twitter:description" content="<%= social_description %>" /> <meta name="twitter:description" content="<%= local_assigns[:social_description] || setting['meta_description'] %>"/>
<meta name="twitter:image" content="<%= image_url local_assigns[:twitter_image_url] || image_path_for('social_media_icon_twitter.png') %>" /> <meta name="twitter:image" content="<%= root_url + (local_assigns[:twitter_image_url] || 'social_media_icon_twitter.png') %>"/>
<!-- Facebook OG --> <!-- Facebook OG -->
<meta id="ogtitle" property="og:title" content="<%= social_title %>"/> <meta id="ogtitle" property="og:title" content="<%= local_assigns[:social_title] || setting['meta_title'] %>"/>
<% if setting['url'] %> <% if setting['url'] %>
<meta property="article:publisher" content=<%= setting['url'] %>/> <meta property="article:publisher" content="<%= setting['url'] %>"/>
<% end %> <% end %>
<% if setting['facebook_handle'] %> <% if setting['facebook_handle'] %>
<meta property="article:author" content="https://www.facebook.com/<%= setting['facebook_handle'] %>"/> <meta property="article:author" content="https://www.facebook.com/<%= setting['facebook_handle'] %>"/>
<% end %> <% end %>
<meta property="og:type" content="article"/> <meta property="og:type" content="article"/>
<meta id="ogurl" property="og:url" content="<%= social_url %>"/> <meta id="ogurl" property="og:url" content="<%= social_url %>"/>
<meta id="ogimage" property="og:image" content="<%= image_url local_assigns[:og_image_url] || image_path_for('social_media_icon.png') %>"/> <meta id="ogimage" property="og:image" content="<%= root_url + (local_assigns[:og_image_url] || 'social_media_icon.png') %>"/>
<meta property="og:site_name" content="<%= setting['org_name'] %>"/> <meta property="og:site_name" content="<%= setting['org_name'] %>"/>
<meta id="ogdescription" property="og:description" content="<%= social_description %>"/> <meta id="ogdescription" property="og:description" content="<%= local_assigns[:social_description] || setting['meta_description'] %>"/>
<meta property="fb:app_id" content="<%= Rails.application.secrets.facebook_key %>"/> <meta property="fb:app_id" content="<%= Rails.application.secrets.facebook_key %>"/>

View File

@@ -4,6 +4,11 @@
<%= render "shared/canonical", href: root_url %> <%= render "shared/canonical", href: root_url %>
<% end %> <% end %>
<% provide :social_media_meta_tags do %>
<%= render "shared/social_media_meta_tags",
social_url: root_url %>
<% end %>
<div class="jumbo highlight"> <div class="jumbo highlight">
<div class="row"> <div class="row">
<div class="small-12 medium-9 small-centered column text-center"> <div class="small-12 medium-9 small-centered column text-center">

9
bin/knapsack_pro_rspec Executable file
View File

@@ -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

View File

@@ -288,3 +288,8 @@ en:
image: image:
image_width: "Width must be %{required_width}px" image_width: "Width must be %{required_width}px"
image_height: "Height must be %{required_height}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"

View File

@@ -104,6 +104,7 @@ en:
table_heading: Heading table_heading: Heading
table_amount: Amount table_amount: Amount
table_population: Population 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: winners:
calculate: Calculate Winner Investments calculate: Calculate Winner Investments
calculated: Winners being calculated, it may take a minute. calculated: Winners being calculated, it may take a minute.
@@ -157,6 +158,7 @@ en:
dossier: Dossier dossier: Dossier
edit_dossier: Edit dossier edit_dossier: Edit dossier
tags: Tags tags: Tags
user_tags: User tags
undefined: Undefined undefined: Undefined
milestone: Milestone milestone: Milestone
new_milestone: Create new milestone new_milestone: Create new milestone
@@ -181,6 +183,7 @@ en:
assigned_valuators: Valuators assigned_valuators: Valuators
select_heading: Select heading select_heading: Select heading
submit_button: Update submit_button: Update
user_tags: User assigned tags
tags: Tags tags: Tags
tags_placeholder: "Write the tags you want separated by commas (,)" tags_placeholder: "Write the tags you want separated by commas (,)"
undefined: Undefined undefined: Undefined
@@ -481,6 +484,7 @@ en:
valuator: valuator:
description_placeholder: 'Description (optional)' description_placeholder: 'Description (optional)'
add: Add to valuators add: Add to valuators
delete: Delete
search: search:
title: 'Valuators: User search' title: 'Valuators: User search'
summary: summary:
@@ -555,6 +559,8 @@ en:
assign: Assign booth assign: Assign booth
unassign: Unassign booth unassign: Unassign booth
poll_booth_assignments: 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: flash:
destroy: "Booth not assigned anymore" destroy: "Booth not assigned anymore"
create: "Booth assigned" create: "Booth assigned"
@@ -952,6 +958,7 @@ en:
direct_messages: Direct messages direct_messages: Direct messages
proposal_notifications: Proposal notifications proposal_notifications: Proposal notifications
incomplete_verifications: Incomplete verifications incomplete_verifications: Incomplete verifications
polls: Polls
direct_messages: direct_messages:
title: Direct messages title: Direct messages
total: Total total: Total
@@ -960,6 +967,17 @@ en:
title: Proposal notifications title: Proposal notifications
total: Total total: Total
proposals_with_notifications: Proposals with notifications 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: tags:
create: Create topic create: Create topic
destroy: Destroy topic destroy: Destroy topic

View File

@@ -48,6 +48,7 @@ en:
map_zoom: Zoom map_zoom: Zoom
mailer_from_name: Origin email name mailer_from_name: Origin email name
mailer_from_address: Origin email address mailer_from_address: Origin email address
meta_title: "Site title (SEO)"
meta_description: "Site description (SEO)" meta_description: "Site description (SEO)"
meta_keywords: "Keywords (SEO)" meta_keywords: "Keywords (SEO)"
verification_offices_url: Verification offices URL verification_offices_url: Verification offices URL

View File

@@ -283,3 +283,8 @@ es:
image: image:
image_width: "Debe tener %{required_width}px de ancho" image_width: "Debe tener %{required_width}px de ancho"
image_height: "Debe tener %{required_height}px de alto" 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

View File

@@ -104,6 +104,7 @@ es:
table_heading: Partida table_heading: Partida
table_amount: Cantidad table_amount: Cantidad
table_population: Población 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: winners:
calculate: Calcular propuestas ganadoras calculate: Calcular propuestas ganadoras
calculated: Calculando ganadoras, puede tardar un minuto. calculated: Calculando ganadoras, puede tardar un minuto.
@@ -157,6 +158,7 @@ es:
dossier: Informe dossier: Informe
edit_dossier: Editar informe edit_dossier: Editar informe
tags: Etiquetas tags: Etiquetas
user_tags: Etiquetas del usuario
undefined: Sin definir undefined: Sin definir
milestone: Seguimiento milestone: Seguimiento
new_milestone: Crear nuevo hito new_milestone: Crear nuevo hito
@@ -181,6 +183,7 @@ es:
assigned_valuators: Evaluadores assigned_valuators: Evaluadores
select_heading: Seleccionar partida select_heading: Seleccionar partida
submit_button: Actualizar submit_button: Actualizar
user_tags: Etiquetas asignadas por el usuario
tags: Etiquetas tags: Etiquetas
tags_placeholder: "Escribe las etiquetas que desees separadas por comas (,)" tags_placeholder: "Escribe las etiquetas que desees separadas por comas (,)"
undefined: Sin definir undefined: Sin definir
@@ -481,6 +484,7 @@ es:
valuator: valuator:
description_placeholder: 'Descripción (opcional)' description_placeholder: 'Descripción (opcional)'
add: Añadir como evaluador add: Añadir como evaluador
delete: Borrar
search: search:
title: 'Evaluadores: Búsqueda de usuarios' title: 'Evaluadores: Búsqueda de usuarios'
summary: summary:
@@ -553,8 +557,10 @@ es:
unassigned: No asignada unassigned: No asignada
actions: actions:
assign: Asignar urna assign: Asignar urna
unassign: Asignar urna unassign: Desasignar urna
poll_booth_assignments: poll_booth_assignments:
alert:
shifts: "Hay turnos asignados para esta urna. Si la desasignas, esos turnos se eliminarán. ¿Deseas continuar?"
flash: flash:
destroy: "Urna desasignada" destroy: "Urna desasignada"
create: "Urna asignada" create: "Urna asignada"
@@ -952,6 +958,7 @@ es:
direct_messages: Mensajes directos direct_messages: Mensajes directos
proposal_notifications: Notificaciones de propuestas proposal_notifications: Notificaciones de propuestas
incomplete_verifications: Verificaciones incompletas incomplete_verifications: Verificaciones incompletas
polls: Votaciones
direct_messages: direct_messages:
title: Mensajes directos title: Mensajes directos
total: Total total: Total
@@ -960,6 +967,17 @@ es:
title: Notificaciones de propuestas title: Notificaciones de propuestas
total: Total total: Total
proposals_with_notifications: Propuestas con notificaciones 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: tags:
create: Crear tema create: Crear tema
destroy: Eliminar tema destroy: Eliminar tema

View File

@@ -48,6 +48,7 @@ es:
map_zoom: Zoom map_zoom: Zoom
mailer_from_name: Nombre email remitente mailer_from_name: Nombre email remitente
mailer_from_address: Dirección email remitente mailer_from_address: Dirección email remitente
meta_title: "Título del sitio (SEO)"
meta_description: "Descripción del sitio (SEO)" meta_description: "Descripción del sitio (SEO)"
meta_keywords: "Palabras clave (SEO)" meta_keywords: "Palabras clave (SEO)"
verification_offices_url: URL oficinas verificación verification_offices_url: URL oficinas verificación

View File

@@ -36,6 +36,8 @@ Rails.application.routes.draw do
get '/welcome', to: 'welcome#welcome' get '/welcome', to: 'welcome#welcome'
get '/cuentasegura', to: 'welcome#verification', as: :cuentasegura get '/cuentasegura', to: 'welcome#verification', as: :cuentasegura
get '/consul.json', to: "installation#details"
resources :debates do resources :debates do
member do member do
post :vote post :vote
@@ -272,7 +274,7 @@ Rails.application.routes.draw do
get :search, on: :collection get :search, on: :collection
end end
resources :valuators, only: [:index, :create] do resources :valuators, only: [:index, :create, :destroy] do
get :search, on: :collection get :search, on: :collection
get :summary, on: :collection get :summary, on: :collection
end end
@@ -339,6 +341,7 @@ Rails.application.routes.draw do
resource :stats, only: :show do resource :stats, only: :show do
get :proposal_notifications, on: :collection get :proposal_notifications, on: :collection
get :direct_messages, on: :collection get :direct_messages, on: :collection
get :polls, on: :collection
end end
namespace :legislation do namespace :legislation do

View File

@@ -50,11 +50,13 @@ section "Creating Settings" do
Setting.create(key: 'feature.user.recommendations', value: "true") Setting.create(key: 'feature.user.recommendations', value: "true")
Setting.create(key: 'feature.community', value: "true") Setting.create(key: 'feature.community', value: "true")
Setting.create(key: 'feature.map', 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_head', value: "")
Setting.create(key: 'per_page_code_body', value: "") Setting.create(key: 'per_page_code_body', value: "")
Setting.create(key: 'comments_body_max_length', value: '1000') Setting.create(key: 'comments_body_max_length', value: '1000')
Setting.create(key: 'mailer_from_name', value: 'CONSUL') Setting.create(key: 'mailer_from_name', value: 'CONSUL')
Setting.create(key: 'mailer_from_address', value: 'noreply@consul.dev') 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_description', value: 'Citizen Participation and Open Government Application')
Setting.create(key: 'meta_keywords', value: 'citizen participation, open government') Setting.create(key: 'meta_keywords', value: 'citizen participation, open government')
Setting.create(key: 'verification_offices_url', value: 'http://oficinas-atencion-ciudadano.url/') 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_latitude', value: 51.48)
Setting.create(key: 'map_longitude', value: 0.0) Setting.create(key: 'map_longitude', value: 0.0)
Setting.create(key: 'map_zoom', value: 10) Setting.create(key: 'map_zoom', value: 10)
Setting.create(key: 'related_contents_report_threshold', value: 2)
end end
section "Creating Geozones" do section "Creating Geozones" do

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class AddTimeReportedToRelatedContent < ActiveRecord::Migration
def change
add_column :related_contents, :times_reported, :integer, default: 0
end
end

View File

@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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", ["title"], name: "index_proposals_on_title", using: :btree
add_index "proposals", ["tsv"], name: "index_proposals_on_tsv", using: :gin 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| create_table "settings", force: :cascade do |t|
t.string "key" t.string "key"
t.string "value" t.string "value"

View File

@@ -65,6 +65,7 @@ Setting["org_name"] = "CONSUL"
Setting["place_name"] = "CONSUL-land" Setting["place_name"] = "CONSUL-land"
# Meta tags for SEO # Meta tags for SEO
Setting["meta_title"] = nil
Setting["meta_description"] = nil Setting["meta_description"] = nil
Setting["meta_keywords"] = nil Setting["meta_keywords"] = nil
@@ -115,3 +116,6 @@ Setting['proposal_improvement_path'] = nil
Setting['map_latitude'] = 51.48 Setting['map_latitude'] = 51.48
Setting['map_longitude'] = 0.0 Setting['map_longitude'] = 0.0
Setting['map_zoom'] = 10 Setting['map_zoom'] = 10
# Related content
Setting['related_contents_report_threshold'] = 5

View File

@@ -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

View File

@@ -882,4 +882,7 @@ LOREM_IPSUM
end end
end end
factory :related_content do
end
end end

View File

@@ -421,20 +421,37 @@ feature 'Admin budget investments' do
end end
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 = create(:budget_investment, tag_list: 'Park')
budget_investment.set_tag_list_on(:valuation, 'Education') budget_investment.set_tag_list_on(:valuation, 'Education')
budget_investment.save budget_investment.save
visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment) visit admin_budget_budget_investment_path(budget_investment.budget, budget_investment)
expect(page).to have_content "Education" within("#user-tags") do
expect(page).to_not have_content "Park" expect(page).to_not have_content "Education"
expect(page).to have_content "Park"
end
click_link 'Edit classification' click_link 'Edit classification'
expect(page).to have_content "Education" 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 "Park"
expect(page).to_not have_content "Trees"
end
end end
scenario "Maintains user tags" do scenario "Maintains user tags" do

View File

@@ -106,6 +106,41 @@ feature 'Admin booths assignments' do
expect(page).to have_content 'There are no booths assigned to this poll.' expect(page).to have_content 'There are no booths assigned to this poll.'
expect(page).not_to have_content booth.name expect(page).not_to have_content booth.name
end 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 end
feature 'Show' do feature 'Show' do

View File

@@ -162,4 +162,99 @@ feature 'Stats' do
end 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 end

View File

@@ -29,6 +29,14 @@ feature 'Admin valuators' do
end end
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 context 'Search' do
background do background do

View File

@@ -1,4 +1,5 @@
require 'rails_helper' require 'rails_helper'
require 'sessions_helper'
feature 'Budget Investments' do feature 'Budget Investments' do
@@ -170,6 +171,25 @@ feature 'Budget Investments' do
expect(order).to eq(new_order) expect(order).to eq(new_order)
end 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 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: 'Best proposal').update_column(:confidence_score, 10)
create(:budget_investment, heading: heading, title: 'Worst proposal').update_column(:confidence_score, 2) 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') expect(current_url).to include('page=1')
end 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 end
context 'Phase I - Accepting' do context 'Phase I - Accepting' do

View File

@@ -38,7 +38,7 @@ feature 'Proposals' do
within("ul.pagination") do within("ul.pagination") do
expect(page).to have_content("1") 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") expect(page).to_not have_content("3")
click_link "Next", exact: false click_link "Next", exact: false
end end
@@ -153,8 +153,8 @@ feature 'Proposals' do
proposal = create(:proposal) proposal = create(:proposal)
visit proposal_path(proposal) visit proposal_path(proposal)
expect(page.html).to include "<meta name=\"twitter:title\" content=\"#{proposal.title}\" />" expect(page).to have_css "meta[name='twitter:title'][content=\"#{proposal.title}\"]", visible: false
expect(page.html).to include "<meta id=\"ogtitle\" property=\"og:title\" content=\"#{proposal.title}\"/>" expect(page).to have_css "meta[property='og:title'][content=\"#{proposal.title}\"]", visible: false
end end
scenario 'Create' do scenario 'Create' do

View File

@@ -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

View File

@@ -282,15 +282,16 @@ describe 'ConsulSchema' do
end end
describe 'Comments' do 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)) proposal_comment = create(:comment, commentable: create(:proposal))
debate_comment = create(:comment, commentable: create(:debate)) 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) spending_proposal_comment = build(:comment, commentable: create(:spending_proposal)).save(skip_validation: true)
response = execute('{ comments { edges { node { commentable_type } } } }') response = execute('{ comments { edges { node { commentable_type } } } }')
received_commentables = extract_fields(response, 'comments', '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 end
it 'displays comments of authors even if public activity is set to false' do 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] expect(received_comments).to match_array [visible_debate_comment.body]
end 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 it 'does not include comments of debates that are not public' do
not_public_debate = create(:debate, :hidden) not_public_debate = create(:debate, :hidden)
not_public_debate_comment = create(:comment, commentable: not_public_debate) 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) expect(received_comments).to_not include(not_public_proposal_comment.body)
end 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 it 'only returns date and hour for created_at' do
created_at = Time.zone.parse("2017-12-31 9:30:15") created_at = Time.zone.parse("2017-12-31 9:30:15")
create(:comment, created_at: created_at) create(:comment, created_at: created_at)

View File

@@ -25,4 +25,23 @@ describe Budget::Heading do
end end
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 end

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,8 @@ require 'factory_girl_rails'
require 'database_cleaner' require 'database_cleaner'
require 'email_spec' require 'email_spec'
require 'devise' require 'devise'
require 'knapsack' require 'knapsack_pro'
Dir["./spec/models/concerns/*.rb"].each { |f| require f } Dir["./spec/models/concerns/*.rb"].each { |f| require f }
Dir["./spec/support/**/*.rb"].sort.each { |f| require f } Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
Dir["./spec/shared/**/*.rb"].sort.each { |f| require f } Dir["./spec/shared/**/*.rb"].sort.each { |f| require f }
@@ -107,4 +108,4 @@ RSpec.configure do |config|
end end
# Parallel build helper configuration for travis # Parallel build helper configuration for travis
Knapsack::Adapters::RSpecAdapter.bind KnapsackPro::Adapters::RSpecAdapter.bind