diff --git a/app/assets/stylesheets/legislation_process.scss b/app/assets/stylesheets/legislation_process.scss index 0e73321d3..e536bb280 100644 --- a/app/assets/stylesheets/legislation_process.scss +++ b/app/assets/stylesheets/legislation_process.scss @@ -109,16 +109,13 @@ $border-dark: darken($border, 10%); li { cursor: pointer; display: inline-block; - margin: 0 1rem 1rem 0; + margin-bottom: $line-height; + margin-right: $line-height; transition: all 0.4s; border-bottom: 2px solid transparent; @include breakpoint(medium) { - margin-left: $line-height * 2; - } - - &:first-of-type { - margin-left: 0; + margin-bottom: 0; } &:hover, diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 81757d78c..40d546999 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -855,7 +855,9 @@ } .debate, -.debate-show { +.debate-show, +.proposal-show, +.legislation-proposals { .votes { @include votes; @@ -870,6 +872,7 @@ } } +.proposal-show .votes, .debate-show .votes { border: 0; padding: $line-height / 2 0; diff --git a/app/controllers/admin/legislation/processes_controller.rb b/app/controllers/admin/legislation/processes_controller.rb index 5d8d828cd..0e7555211 100644 --- a/app/controllers/admin/legislation/processes_controller.rb +++ b/app/controllers/admin/legislation/processes_controller.rb @@ -19,8 +19,10 @@ class Admin::Legislation::ProcessesController < Admin::Legislation::BaseControll def update if @process.update(process_params) + set_tag_list + link = legislation_process_path(@process).html_safe - redirect_to edit_admin_legislation_process_path(@process), notice: t('admin.legislation.processes.update.notice', link: link) + redirect_to :back, notice: t('admin.legislation.processes.update.notice', link: link) else flash.now[:error] = t('admin.legislation.processes.update.error') render :edit @@ -47,12 +49,22 @@ class Admin::Legislation::ProcessesController < Admin::Legislation::BaseControll :draft_publication_date, :allegations_start_date, :allegations_end_date, + :proposals_phase_start_date, + :proposals_phase_end_date, :result_publication_date, :debate_phase_enabled, :allegations_phase_enabled, + :proposals_phase_enabled, :draft_publication_enabled, :result_publication_enabled, - :published + :published, + :proposals_description, + :custom_list ) end + + def set_tag_list + @process.set_tag_list_on(:customs, process_params[:custom_list]) + @process.save + end end diff --git a/app/controllers/admin/legislation/proposals_controller.rb b/app/controllers/admin/legislation/proposals_controller.rb new file mode 100644 index 000000000..7f4441bce --- /dev/null +++ b/app/controllers/admin/legislation/proposals_controller.rb @@ -0,0 +1,7 @@ +class Admin::Legislation::ProposalsController < Admin::Legislation::BaseController + load_and_authorize_resource :process, class: "Legislation::Process" + load_and_authorize_resource :proposal, class: "Legislation::Proposal", through: :process + + def index + end +end diff --git a/app/controllers/legislation/base_controller.rb b/app/controllers/legislation/base_controller.rb index ca609ecba..c08ffc444 100644 --- a/app/controllers/legislation/base_controller.rb +++ b/app/controllers/legislation/base_controller.rb @@ -2,4 +2,8 @@ class Legislation::BaseController < ApplicationController include FeatureFlags feature_flag :legislation + + def set_legislation_proposal_votes(proposals) + @legislation_proposal_votes = current_user ? current_user.legislation_proposal_votes(proposals) : {} + end end diff --git a/app/controllers/legislation/processes_controller.rb b/app/controllers/legislation/processes_controller.rb index 588ffdf9e..d788d3bf3 100644 --- a/app/controllers/legislation/processes_controller.rb +++ b/app/controllers/legislation/processes_controller.rb @@ -14,6 +14,8 @@ class Legislation::ProcessesController < Legislation::BaseController redirect_to legislation_process_draft_version_path(@process, draft_version) elsif @process.debate_phase.enabled? redirect_to debate_legislation_process_path(@process) + elsif @process.proposals_phase.enabled? + redirect_to proposals_legislation_process_path(@process) else redirect_to allegations_legislation_process_path(@process) end @@ -81,6 +83,18 @@ class Legislation::ProcessesController < Legislation::BaseController end end + def proposals + set_process + @phase = :proposals_phase + + if @process.proposals_phase.started? + set_legislation_proposal_votes(@process.proposals) + render :proposals + else + render :phase_not_open + end + end + private def member_method? diff --git a/app/controllers/legislation/proposals_controller.rb b/app/controllers/legislation/proposals_controller.rb new file mode 100644 index 000000000..6aec36057 --- /dev/null +++ b/app/controllers/legislation/proposals_controller.rb @@ -0,0 +1,70 @@ +class Legislation::ProposalsController < Legislation::BaseController + include CommentableActions + include FlagActions + + load_and_authorize_resource :process, class: "Legislation::Process" + load_and_authorize_resource :proposal, class: "Legislation::Proposal", through: :process + + before_action :parse_tag_filter, only: :index + before_action :load_categories, only: [:index, :new, :create, :edit, :map, :summary] + before_action :load_geozones, only: [:edit, :map, :summary] + before_action :authenticate_user!, except: [:index, :show, :map, :summary] + + invisible_captcha only: [:create, :update], honeypot: :subtitle + + has_orders %w{confidence_score created_at}, only: :index + has_orders %w{most_voted newest oldest}, only: :show + + helper_method :resource_model, :resource_name + respond_to :html, :js + + def show + super + set_legislation_proposal_votes(@process.proposals) + @document = Document.new(documentable: @proposal) + redirect_to legislation_process_proposal_path(params[:process_id], @proposal), + status: :moved_permanently if request.path != legislation_process_proposal_path(params[:process_id], @proposal) + end + + def create + @proposal = Legislation::Proposal.new(proposal_params.merge(author: current_user)) + + if @proposal.save + redirect_to legislation_process_proposal_path(params[:process_id], @proposal), notice: I18n.t('flash.actions.create.proposal') + else + render :new + end + end + + def index_customization + load_successful_proposals + load_featured unless @proposal_successful_exists + end + + def vote + @proposal.register_vote(current_user, params[:value]) + set_legislation_proposal_votes(@proposal) + end + + private + + def proposal_params + params.require(:legislation_proposal).permit(:legislation_process_id, :title, + :question, :summary, :description, :video_url, :tag_list, + :terms_of_service, :geozone_id, + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id] ) + end + + def resource_model + Legislation::Proposal + end + + def resource_name + 'proposal' + end + + def load_successful_proposals + @proposal_successful_exists = Legislation::Proposal.successful.exists? + end + +end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 37a186ff1..d71173060 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -17,7 +17,7 @@ module AdminHelper end def menu_moderated_content? - ["proposals", "debates", "comments", "hidden_users"].include? controller_name + ["proposals", "debates", "comments", "hidden_users"].include? controller_name && controller.class.parent != Admin::Legislation end def menu_budget? diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 4cabae278..b31f3167f 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -8,6 +8,8 @@ module TagsHelper proposals_path(search: tag_name) when 'budget/investment' budget_investments_path(@budget, search: tag_name) + when 'legislation/proposal' + legislation_process_proposals_path(@process, search: tag_name) else '#' end @@ -22,6 +24,8 @@ module TagsHelper proposal_path(taggable) when 'budget/investment' budget_investment_path(taggable.budget_id, taggable) + when 'legislation/proposal' + legislation_process_proposal_path(@process, taggable) else '#' end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 1274229b5..951f06249 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -14,6 +14,9 @@ module Abilities can :restore, Proposal cannot :restore, Proposal, hidden_at: nil + can :restore, Legislation::Proposal + cannot :restore, Legislation::Proposal, hidden_at: nil + can :restore, User cannot :restore, User, hidden_at: nil @@ -26,6 +29,9 @@ module Abilities can :confirm_hide, Proposal cannot :confirm_hide, Proposal, hidden_at: nil + can :confirm_hide, Legislation::Proposal + cannot :confirm_hide, Legislation::Proposal, hidden_at: nil + can :confirm_hide, User cannot :confirm_hide, User, hidden_at: nil @@ -33,7 +39,7 @@ module Abilities can :unmark_featured, Debate can :comment_as_administrator, [Debate, Comment, Proposal, Poll::Question, Budget::Investment, - Legislation::Question, Legislation::Annotation, Topic] + Legislation::Question, Legislation::Proposal, Legislation::Annotation, Topic] can [:search, :create, :index, :destroy], ::Administrator can [:search, :create, :index, :destroy], ::Moderator @@ -71,7 +77,8 @@ module Abilities can [:manage], ::Legislation::Process can [:manage], ::Legislation::DraftVersion can [:manage], ::Legislation::Question - cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation] + can [:manage], ::Legislation::Proposal + cannot :comment_as_moderator, [::Legislation::Question, Legislation::Annotation, ::Legislation::Proposal] can [:create, :destroy], Document can [:destroy], Image diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb index e4e92abaf..8f289f986 100644 --- a/app/models/abilities/common.rb +++ b/app/models/abilities/common.rb @@ -18,12 +18,20 @@ module Abilities end can [:retire_form, :retire], Proposal, author_id: user.id + can :read, Legislation::Proposal + cannot [:edit, :update], Legislation::Proposal do |proposal| + proposal.editable_by?(user) + end + can [:retire_form, :retire], Legislation::Proposal, author_id: user.id + can :create, Comment can :create, Debate can :create, Proposal + can :create, Legislation::Proposal can :suggest, Debate can :suggest, Proposal + can :suggest, Legislation::Proposal can :suggest, ActsAsTaggableOn::Tag can [:flag, :unflag], Comment @@ -35,6 +43,9 @@ module Abilities can [:flag, :unflag], Proposal cannot [:flag, :unflag], Proposal, author_id: user.id + can [:flag, :unflag], Legislation::Proposal + cannot [:flag, :unflag], Legislation::Proposal, author_id: user.id + can [:create, :destroy], Follow can [:destroy], Document, documentable: { author_id: user.id } @@ -54,6 +65,10 @@ module Abilities can :vote, SpendingProposal can :create, SpendingProposal + can :vote, Legislation::Proposal + can :vote_featured, Legislation::Proposal + can :create, Legislation::Answer + can :create, Budget::Investment, budget: { phase: "accepting" } can :suggest, Budget::Investment, budget: { phase: "accepting" } can :destroy, Budget::Investment, budget: { phase: ["accepting", "reviewing"] }, author_id: user.id diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index dd692c269..a3720803e 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -24,10 +24,10 @@ module Abilities can [:read, :print], Budget::Investment can :read_results, Budget, phase: "finished" can :new, DirectMessage - can [:read, :debate, :draft_publication, :allegations, :result_publication], Legislation::Process, published: true + can [:read, :debate, :draft_publication, :allegations, :result_publication, :proposals], Legislation::Process, published: true can [:read, :changes, :go_to_version], Legislation::DraftVersion can [:read], Legislation::Question - can [:create], Legislation::Answer + can [:read, :map, :share], Legislation::Proposal can [:search, :comments, :read, :create, :new_comment], Legislation::Annotation end end diff --git a/app/models/abilities/moderation.rb b/app/models/abilities/moderation.rb index e9a1da2ac..f0f823de1 100644 --- a/app/models/abilities/moderation.rb +++ b/app/models/abilities/moderation.rb @@ -38,6 +38,15 @@ module Abilities can :moderate, Proposal cannot :moderate, Proposal, author_id: user.id + can :hide, Legislation::Proposal, hidden_at: nil + cannot :hide, Legislation::Proposal, author_id: user.id + + can :ignore_flag, Legislation::Proposal, ignored_flag_at: nil, hidden_at: nil + cannot :ignore_flag, Legislation::Proposal, author_id: user.id + + can :moderate, Legislation::Proposal + cannot :moderate, Legislation::Proposal, author_id: user.id + can :hide, User cannot :hide, User, id: user.id diff --git a/app/models/abilities/moderator.rb b/app/models/abilities/moderator.rb index 4e1427c12..eac434f22 100644 --- a/app/models/abilities/moderator.rb +++ b/app/models/abilities/moderator.rb @@ -6,7 +6,7 @@ module Abilities merge Abilities::Moderation.new(user) can :comment_as_moderator, [Debate, Comment, Proposal, Budget::Investment, Poll::Question, - Legislation::Question, Legislation::Annotation, Topic] + Legislation::Question, Legislation::Annotation, Legislation::Proposal, Topic] end end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 37fa9b630..8e3024d06 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -3,7 +3,7 @@ class Comment < ActiveRecord::Base include HasPublicAuthor include Graphqlable - COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic Poll).freeze + COMMENTABLE_TYPES = %w(Debate Proposal Budget::Investment Poll::Question Legislation::Question Legislation::Annotation Topic Legislation::Proposal Poll).freeze acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb index b6043df57..d6eed62db 100644 --- a/app/models/legislation/process.rb +++ b/app/models/legislation/process.rb @@ -1,14 +1,18 @@ class Legislation::Process < ActiveRecord::Base - acts_as_paranoid column: :hidden_at include ActsAsParanoidAliases + include Taggable - PHASES_AND_PUBLICATIONS = %i(debate_phase allegations_phase draft_publication result_publication).freeze + acts_as_paranoid column: :hidden_at + acts_as_taggable_on :customs + + PHASES_AND_PUBLICATIONS = %i(debate_phase allegations_phase proposals_phase draft_publication result_publication).freeze has_many :draft_versions, -> { order(:id) }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id', dependent: :destroy has_one :final_draft_version, -> { where final_version: true, status: 'published' }, class_name: 'Legislation::DraftVersion', foreign_key: 'legislation_process_id' has_many :questions, -> { order(:id) }, class_name: 'Legislation::Question', foreign_key: 'legislation_process_id', dependent: :destroy + has_many :proposals, -> { order(:id) }, class_name: 'Legislation::Proposal', foreign_key: 'legislation_process_id', dependent: :destroy validates :title, presence: true validates :start_date, presence: true @@ -17,6 +21,7 @@ class Legislation::Process < ActiveRecord::Base validates :debate_end_date, presence: true, if: :debate_start_date? validates :allegations_start_date, presence: true, if: :allegations_end_date? validates :allegations_end_date, presence: true, if: :allegations_start_date? + validates :proposals_phase_end_date, presence: true, if: :proposals_phase_start_date? validate :valid_date_ranges scope :open, -> { where("start_date <= ? and end_date >= ?", Date.current, Date.current).order('id DESC') } @@ -33,6 +38,10 @@ class Legislation::Process < ActiveRecord::Base Legislation::Process::Phase.new(allegations_start_date, allegations_end_date, allegations_phase_enabled) end + def proposals_phase + Legislation::Process::Phase.new(proposals_phase_start_date, proposals_phase_end_date, proposals_phase_enabled) + end + def draft_publication Legislation::Process::Publication.new(draft_publication_date, draft_publication_enabled) end diff --git a/app/models/legislation/proposal.rb b/app/models/legislation/proposal.rb new file mode 100644 index 000000000..ec3dc6410 --- /dev/null +++ b/app/models/legislation/proposal.rb @@ -0,0 +1,147 @@ +class Legislation::Proposal < ActiveRecord::Base + include ActsAsParanoidAliases + include Flaggable + include Taggable + include Conflictable + include Measurable + include Sanitizable + include Searchable + include Filterable + include Followable + include Communitable + include Documentable + + documentable max_documents_allowed: 3, + max_file_size: 3.megabytes, + accepted_content_types: [ "application/pdf" ] + accepts_nested_attributes_for :documents, allow_destroy: true + + acts_as_votable + acts_as_paranoid column: :hidden_at + + belongs_to :process, class_name: 'Legislation::Process', foreign_key: 'legislation_process_id' + belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id' + belongs_to :geozone + has_many :comments, as: :commentable + + validates :title, presence: true + validates :summary, presence: true + validates :author, presence: true + + validates :title, length: { in: 4..Legislation::Proposal.title_max_length } + validates :description, length: { maximum: Legislation::Proposal.description_max_length } + + validates :terms_of_service, acceptance: { allow_nil: false }, on: :create + + before_validation :set_responsible_name + + before_save :calculate_hot_score, :calculate_confidence_score + + scope :for_render, -> { includes(:tags) } + scope :sort_by_hot_score, -> { reorder(hot_score: :desc) } + scope :sort_by_confidence_score, -> { reorder(confidence_score: :desc) } + scope :sort_by_created_at, -> { reorder(created_at: :desc) } + scope :sort_by_most_commented, -> { reorder(comments_count: :desc) } + scope :sort_by_random, -> { reorder("RANDOM()") } + scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} + + def to_param + "#{id}-#{title}".parameterize + end + + def searchable_values + { title => 'A', + question => 'B', + author.username => 'B', + tag_list.join(' ') => 'B', + geozone.try(:name) => 'B', + summary => 'C', + description => 'D' + } + end + + def self.search(terms) + by_code = search_by_code(terms.strip) + by_code.present? ? by_code : pg_search(terms) + end + + def self.search_by_code(terms) + matched_code = match_code(terms) + results = where(id: matched_code[1]) if matched_code + return results if (results.present? && results.first.code == terms) + end + + def self.match_code(terms) + /\A#{Setting["proposal_code_prefix"]}-\d\d\d\d-\d\d-(\d*)\z/.match(terms) + end + + def likes + cached_votes_up + end + + def dislikes + cached_votes_down + end + + def total_votes + cached_votes_total + end + + def voters + User.active.where(id: votes_for.voters) + end + + def editable? + total_votes <= Setting["max_votes_for_proposal_edit"].to_i + end + + def editable_by?(user) + author_id == user.id && editable? + end + + def votable_by?(user) + user && user.level_two_or_three_verified? + end + + def register_vote(user, vote_value) + if votable_by?(user) + vote_by(voter: user, vote: vote_value) + end + end + + def code + "#{Setting['proposal_code_prefix']}-#{created_at.strftime('%Y-%m')}-#{id}" + end + + def after_commented + save # updates the hot_score because there is a before_save + end + + def calculate_hot_score + self.hot_score = ScoreCalculator.hot_score(created_at, + total_votes, + total_votes, + comments_count) + end + + def calculate_confidence_score + self.confidence_score = ScoreCalculator.confidence_score(total_votes, total_votes) + end + + def after_hide + tags.each{ |t| t.decrement_custom_counter_for('LegislationProposal') } + end + + def after_restore + tags.each{ |t| t.increment_custom_counter_for('LegislationProposal') } + end + + protected + + def set_responsible_name + if author && author.document_number? + self.responsible_name = author.document_number + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 80cf75f61..a212c1b5c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -97,6 +97,12 @@ class User < ActiveRecord::Base voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } end + def legislation_proposal_votes(proposals) + voted = votes.for_legislation_proposals(proposals) + voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } + end + + def spending_proposal_votes(spending_proposals) voted = votes.for_spending_proposals(spending_proposals) voted.each_with_object({}) { |v, h| h[v.votable_id] = v.value } diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index fdc28ec31..bae999fe4 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -19,7 +19,7 @@ <%= t("admin.menu.title_moderated_content") %>
<%= format_date(process.debate_start_date) %> - <%= format_date(process.debate_end_date) %>
@@ -14,8 +14,17 @@<%= format_date(process.proposals_phase_start_date) %> - <%= format_date(process.proposals_phase_end_date) %>
+ <% end %> +<%= format_date(process.draft_publication_date) %>
@@ -24,7 +33,7 @@ <% end %> <% if process.allegations_phase.enabled? %> -<%= format_date(process.allegations_start_date) %> - <%= format_date(process.allegations_end_date) %>
@@ -33,7 +42,7 @@ <% end %> <% if process.result_publication.enabled? %> -<%= format_date(process.result_publication_date) %>
diff --git a/app/views/legislation/processes/_process.html.erb b/app/views/legislation/processes/_process.html.erb index d5e44076a..a41fdbe4a 100644 --- a/app/views/legislation/processes/_process.html.erb +++ b/app/views/legislation/processes/_process.html.erb @@ -40,6 +40,13 @@<%= format_date(process.proposals_phase_start_date) %> - <%= format_date(process.proposals_phase_end_date) %>
+<%= t('.empty_proposals') %>
+<%= link_to t("proposals.index.start_proposal"), new_legislation_process_proposal_path(@process), class: 'button expanded' %>
+ <% end %> + <%= render 'legislation/proposals/categories', taggable: @process %> +<%= t("proposals.form.proposal_summary_note") %>
+ <%= f.text_area :summary, rows: 4, maxlength: 200, label: false, + placeholder: t('proposals.form.proposal_summary'), + aria: {describedby: "summary-help-text"} %> +<%= t("proposals.form.proposal_video_url_note") %>
+ <%= f.text_field :video_url, placeholder: t("proposals.form.proposal_video_url"), label: false, + aria: {describedby: "video-url-help-text"} %> +<%= t("proposals.form.tags_instructions") %>
+ + + +<%= notification.created_at.to_date %>
+<%= notification.body %>
+ <% end %> +
+ <%= link_to t("proposals.index.top_link_proposals"), summary_proposals_path, class: "small" %>
+
+ + <%= link_to t("proposals.proposal.comments", count: proposal.comments_count), legislation_process_proposal_path(proposal.legislation_process_id, proposal, anchor: "comments") %> + + • + <%= l proposal.created_at.to_date %> + + <% if proposal.author.hidden? || proposal.author.erased? %> + • + + <% else %> + • + + <% if proposal.author.display_official_position_badge? %> + • + + <% end %> + <% end %> + + <% if proposal.author.verified_organization? %> + • + + <%= t("shared.collective") %> + + <% end %> +
+<%= proposal.summary %>
+ +<%= t("legislation.proposals.closed") %>
+ <% end %> + + + <%= t("proposals.proposal.votes", count: proposal.total_votes) %> + + + <% if user_signed_in? && current_user.organization? %> + + <% elsif user_signed_in? && !proposal.votable_by?(current_user) %> + + <% elsif !user_signed_in? %> ++ <%= t("proposals.show.code") %> + <%= @proposal.code %> +
+ +<%= @proposal.summary %>+ + <% if @proposal.video_url.present? %> +
+ + <%= t('proposals.show.title_external_url') %> +
+ <%= text_with_links @proposal.external_url %> ++ + <%= t('proposals.show.title_video_url') %> +
+ <%= text_with_links @proposal.video_url %> +
+ +