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

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
* 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
### 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

View File

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

View File

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

View File

@@ -4,4 +4,4 @@
require File.expand_path('../config/application', __FILE__)
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 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(){

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

View File

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

View File

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

View File

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

View File

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

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)
number_to_currency(number, precision: 0, locale: I18n.default_locale)
end
def kaminari_path(url)
"#{root_url.chomp("\/")}#{url}"
end
end

View File

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

View File

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

View File

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

View File

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

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 HasPublicAuthor
include Graphqlable
include Relationable
acts_as_votable
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 :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) }

View File

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

View File

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

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 %>
<%= 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 } %>
</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">
<%= f.text_field :external_url %>
</div>
@@ -39,7 +46,7 @@
<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">
<% @tags.each do |tag| %>
<a class="js-add-tag-link"><%= tag.name %></a>

View File

@@ -19,7 +19,7 @@
<p id="tags">
<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 id="assigned_valuators">

View File

@@ -49,10 +49,17 @@
<div class="row">
<div class="small-12 medium-6 column">
<label><%= t("admin.budgets.form.population") %></label>
<%= 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"} %>
</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>

View File

@@ -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? %>
</td>
<% else %>
<td>
@@ -26,6 +27,6 @@
method: :post,
remote: true,
title: t("admin.booth_assignments.manage.actions.assign"),
class: "button" %>
class: "button" if !@poll.expired? %>
</td>
<% 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>
<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"),
direct_messages_admin_stats_path, class: "button hollow" %>
<%= link_to t("admin.stats.show.proposal_notifications"),

View File

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

View File

@@ -29,7 +29,12 @@
<%= t("admin.valuators.index.no_description") %>
<% end %>
<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| %>
<%= f.text_field :description,
label: false,

View File

@@ -1,3 +1,3 @@
<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>

View File

@@ -1,3 +1,3 @@
<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>

View File

@@ -1,3 +1,3 @@
<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>

View File

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

View File

@@ -1,3 +1,3 @@
<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>

View File

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

View File

@@ -1,20 +1,20 @@
<!-- Twitter -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@consul_dev" />
<meta name="twitter:title" content="<%= social_title %>" />
<meta name="twitter:description" content="<%= social_description %>" />
<meta name="twitter:image" content="<%= image_url local_assigns[:twitter_image_url] || image_path_for('social_media_icon_twitter.png') %>" />
<meta name="twitter:card" content="summary"/>
<meta name="twitter:site" content="<%= setting['twitter_handle'] %>"/>
<meta name="twitter:title" content="<%= local_assigns[:social_title] || setting['meta_title'] %>"/>
<meta name="twitter:description" content="<%= local_assigns[:social_description] || setting['meta_description'] %>"/>
<meta name="twitter:image" content="<%= root_url + (local_assigns[:twitter_image_url] || 'social_media_icon_twitter.png') %>"/>
<!-- 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'] %>
<meta property="article:publisher" content=<%= setting['url'] %>/>
<meta property="article:publisher" content="<%= setting['url'] %>"/>
<% end %>
<% if setting['facebook_handle'] %>
<meta property="article:author" content="https://www.facebook.com/<%= setting['facebook_handle'] %>"/>
<% end %>
<meta property="og:type" content="article"/>
<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 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 %>"/>

View File

@@ -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 %>
<div class="jumbo highlight">
<div class="row">
<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_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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
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"

View File

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

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
factory :related_content do
end
end

View File

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

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).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

View File

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

View File

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

View File

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

View File

@@ -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 "<meta name=\"twitter:title\" content=\"#{proposal.title}\" />"
expect(page.html).to include "<meta id=\"ogtitle\" property=\"og:title\" content=\"#{proposal.title}\"/>"
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

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

View File

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

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