diff --git a/app/models/legislation/process.rb b/app/models/legislation/process.rb index 6a37aeef2..7e1e884cb 100644 --- a/app/models/legislation/process.rb +++ b/app/models/legislation/process.rb @@ -18,6 +18,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') } diff --git a/app/models/legislation/proposal.rb b/app/models/legislation/proposal.rb index 2c187d302..1eb82e70a 100644 --- a/app/models/legislation/proposal.rb +++ b/app/models/legislation/proposal.rb @@ -1,8 +1,193 @@ -class Legislation::Proposal < Proposal - acts_as_paranoid column: :hidden_at +class Legislation::Proposal < ActiveRecord::Base include ActsAsParanoidAliases + include Flaggable + include Taggable + include Conflictable + include Measurable + include Sanitizable + include Searchable + include Filterable + include HasPublicAuthor + include Graphqlable + 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 + + RETIRE_OPTIONS = %w(duplicated started unfeasible done other) 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 + has_many :proposal_notifications - scope :sorted, -> { order('id ASC') } + validates :title, presence: true + validates :question, presence: true + validates :summary, presence: true + validates :author, presence: true + validates :responsible_name, presence: true + + validates :title, length: { in: 4..Legislation::Proposal.title_max_length } + validates :description, length: { maximum: Legislation::Proposal.description_max_length } + validates :question, length: { in: 10..Legislation::Proposal.question_max_length } + validates :responsible_name, length: { in: 6..Legislation::Proposal.responsible_name_max_length } + validates :retired_reason, inclusion: {in: RETIRE_OPTIONS, allow_nil: true} + + 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_relevance, -> { all } + scope :sort_by_flags, -> { order(flags_count: :desc, updated_at: :desc) } + scope :sort_by_archival_date, -> { archived.sort_by_confidence_score } + scope :archived, -> { where("proposals.created_at <= ?", Setting["months_to_archive_proposals"].to_i.months.ago) } + scope :not_archived, -> { where("proposals.created_at > ?", Setting["months_to_archive_proposals"].to_i.months.ago) } + scope :last_week, -> { where("proposals.created_at >= ?", 7.days.ago)} + scope :retired, -> { where.not(retired_at: nil) } + scope :not_retired, -> { where(retired_at: nil) } + scope :successful, -> { where("cached_votes_up >= ?", Legislation::Proposal.votes_needed_for_success) } + scope :public_for_api, -> { all } + + 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 self.for_summary + summary = {} + categories = ActsAsTaggableOn::Tag.category_names.sort + geozones = Geozone.names.sort + + groups = categories + geozones + groups.each do |group| + summary[group] = search(group).last_week.sort_by_confidence_score.limit(3) + end + summary + end + + def total_votes + cached_votes_up + 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 retired? + retired_at.present? + end + + def register_vote(user, vote_value) + if votable_by?(user) && !archived? + 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('Proposal') } + end + + def after_restore + tags.each{ |t| t.increment_custom_counter_for('Proposal') } + end + + def self.votes_needed_for_success + Setting['votes_for_proposal_success'].to_i + end + + def successful? + total_votes >= Legislation::Proposal.votes_needed_for_success + end + + def archived? + created_at <= Setting["months_to_archive_proposals"].to_i.months.ago + end + + def notifications + proposal_notifications + end + + def users_to_notify + (voters + followers).uniq + end + + protected + + def set_responsible_name + if author && author.document_number? + self.responsible_name = author.document_number + end + end end diff --git a/db/migrate/20170914102634_create_legislation_proposals.rb b/db/migrate/20170914102634_create_legislation_proposals.rb new file mode 100644 index 000000000..defe9f1dd --- /dev/null +++ b/db/migrate/20170914102634_create_legislation_proposals.rb @@ -0,0 +1,32 @@ +class CreateLegislationProposals < ActiveRecord::Migration + def change + create_table :legislation_proposals do |t| + t.references :legislation_process, index: true, foreign_key: true + t.string :title, limit: 80 + t.text :description + t.string :question + t.string :external_url + t.integer :author_id + t.datetime :hidden_at + t.integer :flags_count, default: 0 + t.datetime :ignored_flag_at + t.integer :cached_votes_up, default: 0 + t.integer :comments_count, default: 0 + t.datetime :confirmed_hide_at + t.integer :hot_score, limit: 8, default: 0 + t.integer :confidence_score, default: 0 + t.string :responsible_name, limit: 60 + t.text :summary + t.string :video_url + t.tsvector :tsv + t.integer :geozone_id + t.datetime :retired_at + t.string :retired_reason + t.text :retired_explanation + t.integer :community_id + + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5b84833a7..8a33adfc8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170913130803) do +ActiveRecord::Schema.define(version: 20170914102634) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -486,6 +486,36 @@ ActiveRecord::Schema.define(version: 20170913130803) do add_index "legislation_processes", ["result_publication_date"], name: "index_legislation_processes_on_result_publication_date", using: :btree add_index "legislation_processes", ["start_date"], name: "index_legislation_processes_on_start_date", using: :btree + create_table "legislation_proposals", force: :cascade do |t| + t.integer "legislation_process_id" + t.string "title", limit: 80 + t.text "description" + t.string "question" + t.string "external_url" + t.integer "author_id" + t.datetime "hidden_at" + t.integer "flags_count", default: 0 + t.datetime "ignored_flag_at" + t.integer "cached_votes_up", default: 0 + t.integer "comments_count", default: 0 + t.datetime "confirmed_hide_at" + t.integer "hot_score", limit: 8, default: 0 + t.integer "confidence_score", default: 0 + t.string "responsible_name", limit: 60 + t.text "summary" + t.string "video_url" + t.tsvector "tsv" + t.integer "geozone_id" + t.datetime "retired_at" + t.string "retired_reason" + t.text "retired_explanation" + t.integer "community_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "legislation_proposals", ["legislation_process_id"], name: "index_legislation_proposals_on_legislation_process_id", using: :btree + create_table "legislation_question_options", force: :cascade do |t| t.integer "legislation_question_id" t.string "value" @@ -1083,6 +1113,7 @@ ActiveRecord::Schema.define(version: 20170913130803) do add_foreign_key "geozones_polls", "polls" add_foreign_key "identities", "users" add_foreign_key "legislation_draft_versions", "legislation_processes" + add_foreign_key "legislation_proposals", "legislation_processes" add_foreign_key "locks", "users" add_foreign_key "managers", "users" add_foreign_key "moderators", "users"