From b359974d9b356bb723fe046466b178328cf9bbaf Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 22 Sep 2019 14:15:18 +0200 Subject: Show user what options they have voted (#11195) * Add own_votes field to poll results in REST API Fixes #10679 * Display user votes in WebUI * Update styling * Add vote checkmark to public pages --- app/serializers/rest/poll_serializer.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'app/serializers/rest') diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb index 356c45b83..eb98bb2d2 100644 --- a/app/serializers/rest/poll_serializer.rb +++ b/app/serializers/rest/poll_serializer.rb @@ -8,6 +8,7 @@ class REST::PollSerializer < ActiveModel::Serializer has_many :emojis, serializer: REST::CustomEmojiSerializer attribute :voted, if: :current_user? + attribute :own_votes, if: :current_user? def id object.id.to_s @@ -21,6 +22,10 @@ class REST::PollSerializer < ActiveModel::Serializer object.voted?(current_user.account) end + def own_votes + object.own_votes(current_user.account) + end + def current_user? !current_user.nil? end -- cgit From 18b451c0e6cf6a927a22084f94b423982de0ee8b Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 27 Sep 2019 21:13:51 +0200 Subject: Change silences to always require approval on follow (#11975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change silenced accounts to require approval on follow * Also require approval for follows by people explicitly muted by target accounts * Do not auto-accept silenced or muted accounts when switching from locked to unlocked * Add `follow_requests_count` to verify_credentials * Show “Follow requests” menu item if needed even if account is locked * Add tests * Correctly reflect that follow requests weren't auto-accepted when local account is silenced * Accept follow requests from user-muted accounts to avoid leaking mutes --- app/controllers/api/v1/accounts_controller.rb | 2 +- .../mastodon/features/getting_started/index.js | 8 ++--- app/lib/activitypub/activity/follow.rb | 2 +- .../rest/credential_account_serializer.rb | 1 + app/services/follow_service.rb | 2 +- app/services/update_account_service.rb | 4 ++- spec/lib/activitypub/activity/follow_spec.rb | 30 +++++++++++++++++ spec/services/follow_service_spec.rb | 27 +++++++++++++++ spec/services/update_account_service_spec.rb | 38 ++++++++++++++++++++++ 9 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 spec/services/update_account_service_spec.rb (limited to 'app/serializers/rest') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b306e8e8c..c12e1c12e 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -33,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController def follow FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs)) - options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } + options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } } render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) end diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index f6d90580b..67ec7665b 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -77,16 +77,14 @@ class GettingStarted extends ImmutablePureComponent { }; componentDidMount () { - const { myAccount, fetchFollowRequests, multiColumn } = this.props; + const { fetchFollowRequests, multiColumn } = this.props; if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { this.context.router.history.replace('/timelines/home'); return; } - if (myAccount.get('locked')) { - fetchFollowRequests(); - } + fetchFollowRequests(); } render () { @@ -134,7 +132,7 @@ class GettingStarted extends ImmutablePureComponent { height += 48*3; - if (myAccount.get('locked')) { + if (myAccount.get('locked') || unreadFollowRequests > 0) { navItems.push(); height += 48; } diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index 28f1da19f..ec92f4255 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -21,7 +21,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id']) - if target_account.locked? + if target_account.locked? || @account.silenced? NotifyService.new.call(target_account, follow_request) else AuthorizeFollowService.new.call(@account, target_account) diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb index fb195eb07..be0d763dc 100644 --- a/app/serializers/rest/credential_account_serializer.rb +++ b/app/serializers/rest/credential_account_serializer.rb @@ -12,6 +12,7 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer language: user.setting_default_language, note: object.note, fields: object.fields.map(&:to_h), + follow_requests_count: FollowRequest.where(target_account: object).limit(40).count, } end end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 101acdaf9..1941c2e2d 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -30,7 +30,7 @@ class FollowService < BaseService ActivityTracker.increment('activity:interactions') - if target_account.locked? || target_account.activitypub? + if target_account.locked? || source_account.silenced? || target_account.activitypub? request_follow(source_account, target_account, reblogs: reblogs) elsif target_account.local? direct_follow(source_account, target_account, reblogs: reblogs) diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb index 01756a73d..ebf24be37 100644 --- a/app/services/update_account_service.rb +++ b/app/services/update_account_service.rb @@ -20,7 +20,9 @@ class UpdateAccountService < BaseService private def authorize_all_follow_requests(account) - AuthorizeFollowWorker.push_bulk(FollowRequest.where(target_account: account).select(:account_id, :target_account_id)) do |req| + follow_requests = FollowRequest.where(target_account: account) + follow_requests = follow_requests.select { |req| !req.account.silenced? } + AuthorizeFollowWorker.push_bulk(follow_requests) do |req| [req.account_id, req.target_account_id] end end diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index 6bbacdbe6..05112cc18 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -31,6 +31,36 @@ RSpec.describe ActivityPub::Activity::Follow do end end + context 'silenced account following an unlocked account' do + before do + sender.touch(:silenced_at) + subject.perform + end + + it 'does not create a follow from sender to recipient' do + expect(sender.following?(recipient)).to be false + end + + it 'creates a follow request' do + expect(sender.requested?(recipient)).to be true + end + end + + context 'unlocked account muting the sender' do + before do + recipient.mute!(sender) + subject.perform + end + + it 'creates a follow from sender to recipient' do + expect(sender.following?(recipient)).to be true + end + + it 'does not create a follow request' do + expect(sender.requested?(recipient)).to be false + end + end + context 'locked account' do before do recipient.update(locked: true) diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 86c85293e..ae863a9f0 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -30,6 +30,33 @@ RSpec.describe FollowService, type: :service do end end + describe 'unlocked account, from silenced account' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + sender.touch(:silenced_at) + subject.call(sender, bob.acct) + end + + it 'creates a follow request with reblogs' do + expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil + end + end + + describe 'unlocked account, from a muted account' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + bob.mute!(sender) + subject.call(sender, bob.acct) + end + + it 'creates a following relation with reblogs' do + expect(sender.following?(bob)).to be true + expect(sender.muting_reblogs?(bob)).to be false + end + end + describe 'unlocked account' do let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } diff --git a/spec/services/update_account_service_spec.rb b/spec/services/update_account_service_spec.rb new file mode 100644 index 000000000..960b26891 --- /dev/null +++ b/spec/services/update_account_service_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe UpdateAccountService, type: :service do + subject { UpdateAccountService.new } + + describe 'switching form locked to unlocked accounts' do + let(:account) { Fabricate(:account, locked: true) } + let(:alice) { Fabricate(:user, email: 'alice@example.com', account: Fabricate(:account, username: 'alice')).account } + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + let(:eve) { Fabricate(:user, email: 'eve@example.com', account: Fabricate(:account, username: 'eve')).account } + + before do + bob.touch(:silenced_at) + account.mute!(eve) + + FollowService.new.call(alice, account) + FollowService.new.call(bob, account) + FollowService.new.call(eve, account) + + subject.call(account, { locked: false }) + end + + it 'auto-accepts pending follow requests' do + expect(alice.following?(account)).to be true + expect(alice.requested?(account)).to be false + end + + it 'does not auto-accept pending follow requests from silenced users' do + expect(bob.following?(account)).to be false + expect(bob.requested?(account)).to be true + end + + it 'auto-accepts pending follow requests from muted users so as to not leak mute' do + expect(eve.following?(account)).to be true + expect(eve.requested?(account)).to be false + end + end +end -- cgit From 3babf8464b0903b854ec16d355909444ef3ca0bc Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 29 Sep 2019 22:58:01 +0200 Subject: Add voters count support (#11917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add voters count to polls * Add ActivityPub serialization and parsing of voters count * Add support for voters count in WebUI * Move incrementation of voters count out of redis lock * Reword “voters” to “people” --- app/javascript/mastodon/components/poll.js | 19 +++++++--- app/lib/activitypub/activity/create.rb | 40 +++++++++++++++++++--- app/lib/activitypub/adapter.rb | 1 + app/models/poll.rb | 1 + app/serializers/activitypub/note_serializer.rb | 12 ++++++- app/serializers/rest/poll_serializer.rb | 2 +- app/services/activitypub/process_poll_service.rb | 5 ++- app/services/post_status_service.rb | 2 +- app/services/vote_service.rb | 32 +++++++++++++++-- app/views/statuses/_poll.html.haml | 8 +++-- config/locales/en.yml | 3 ++ .../20190927232842_add_voters_count_to_polls.rb | 5 +++ db/schema.rb | 3 +- 13 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 db/migrate/20190927232842_add_voters_count_to_polls.rb (limited to 'app/serializers/rest') diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index f88d260f2..cdbcf8f70 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -102,10 +102,11 @@ class Poll extends ImmutablePureComponent { renderOption (option, optionIndex, showResults) { const { poll, disabled, intl } = this.props; - const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); + const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); + const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); let titleEmojified = option.get('title_emojified'); if (!titleEmojified) { @@ -157,6 +158,14 @@ class Poll extends ImmutablePureComponent { const showResults = poll.get('voted') || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + let votesCount = null; + + if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) { + votesCount = ; + } else { + votesCount = ; + } + return (
    @@ -166,7 +175,7 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } {showResults && !this.props.disabled && · } - + {votesCount} {poll.get('expires_at') && · {timeRemaining}}
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index e69193b71..76bf9b2e5 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -232,25 +232,40 @@ class ActivityPub::Activity::Create < ActivityPub::Activity items = @object['oneOf'] end + voters_count = @object['votersCount'] + @account.polls.new( multiple: multiple, expires_at: expires_at, options: items.map { |item| item['name'].presence || item['content'] }.compact, - cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } + cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }, + voters_count: voters_count ) end def poll_vote? return false if replied_to_status.nil? || replied_to_status.preloadable_poll.nil? || !replied_to_status.local? || !replied_to_status.preloadable_poll.options.include?(@object['name']) - unless replied_to_status.preloadable_poll.expired? - replied_to_status.preloadable_poll.votes.create!(account: @account, choice: replied_to_status.preloadable_poll.options.index(@object['name']), uri: @object['id']) - ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals? - end + poll_vote! unless replied_to_status.preloadable_poll.expired? true end + def poll_vote! + poll = replied_to_status.preloadable_poll + already_voted = true + RedisLock.acquire(poll_lock_options) do |lock| + if lock.acquired? + already_voted = poll.votes.where(account: @account).exists? + poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: @object['id']) + else + raise Mastodon::RaceConditionError + end + end + increment_voters_count! unless already_voted + ActivityPub::DistributePollUpdateWorker.perform_in(3.minutes, replied_to_status.id) unless replied_to_status.preloadable_poll.hide_totals? + end + def resolve_thread(status) return unless status.reply? && status.thread.nil? && Request.valid_url?(in_reply_to_uri) ThreadResolveWorker.perform_async(status.id, in_reply_to_uri) @@ -416,7 +431,22 @@ class ActivityPub::Activity::Create < ActivityPub::Activity ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url]) end + def increment_voters_count! + poll = replied_to_status.preloadable_poll + unless poll.voters_count.nil? + poll.voters_count = poll.voters_count + 1 + poll.save + end + rescue ActiveRecord::StaleObjectError + poll.reload + retry + end + def lock_options { redis: Redis.current, key: "create:#{@object['id']}" } end + + def poll_lock_options + { redis: Redis.current, key: "vote:#{replied_to_status.poll_id}:#{@account.id}" } + end end diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index cb2ac72d4..2a8f72333 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -21,6 +21,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' }, blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, + voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, }.freeze def self.default_key_transform diff --git a/app/models/poll.rb b/app/models/poll.rb index 55a8f13a6..5427368fd 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -16,6 +16,7 @@ # created_at :datetime not null # updated_at :datetime not null # lock_version :integer default(0), not null +# voters_count :bigint(8) # class Poll < ApplicationRecord diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 364d3eda5..110621a28 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub::NoteSerializer < ActivityPub::Serializer - context_extensions :atom_uri, :conversation, :sensitive + context_extensions :atom_uri, :conversation, :sensitive, :voters_count attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -23,6 +23,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer attribute :end_time, if: :poll_and_expires? attribute :closed, if: :poll_and_expired? + attribute :voters_count, if: :poll_and_voters_count? + def id ActivityPub::TagManager.instance.uri_for(object) end @@ -141,6 +143,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer alias end_time closed + def voters_count + object.preloadable_poll.voters_count + end + def poll_and_expires? object.preloadable_poll&.expires_at&.present? end @@ -149,6 +155,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer object.preloadable_poll&.expired? end + def poll_and_voters_count? + object.preloadable_poll&.voters_count + end + class MediaAttachmentSerializer < ActivityPub::Serializer context_extensions :blurhash, :focal_point diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb index eb98bb2d2..df6ebd0d4 100644 --- a/app/serializers/rest/poll_serializer.rb +++ b/app/serializers/rest/poll_serializer.rb @@ -2,7 +2,7 @@ class REST::PollSerializer < ActiveModel::Serializer attributes :id, :expires_at, :expired, - :multiple, :votes_count + :multiple, :votes_count, :voters_count has_many :loaded_options, key: :options has_many :emojis, serializer: REST::CustomEmojiSerializer diff --git a/app/services/activitypub/process_poll_service.rb b/app/services/activitypub/process_poll_service.rb index 2fbce65b9..cb4a0d460 100644 --- a/app/services/activitypub/process_poll_service.rb +++ b/app/services/activitypub/process_poll_service.rb @@ -28,6 +28,8 @@ class ActivityPub::ProcessPollService < BaseService end end + voters_count = @json['votersCount'] + latest_options = items.map { |item| item['name'].presence || item['content'] } # If for some reasons the options were changed, it invalidates all previous @@ -39,7 +41,8 @@ class ActivityPub::ProcessPollService < BaseService last_fetched_at: Time.now.utc, expires_at: expires_at, options: latest_options, - cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 } + cached_tallies: items.map { |item| item.dig('replies', 'totalItems') || 0 }, + voters_count: voters_count ) rescue ActiveRecord::StaleObjectError poll.reload diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 34ec6d504..a0a650d62 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -174,7 +174,7 @@ class PostStatusService < BaseService def poll_attributes return if @options[:poll].blank? - @options[:poll].merge(account: @account) + @options[:poll].merge(account: @account, voters_count: 0) end def scheduled_options diff --git a/app/services/vote_service.rb b/app/services/vote_service.rb index 0eeb8fd56..cb7dce6e8 100644 --- a/app/services/vote_service.rb +++ b/app/services/vote_service.rb @@ -12,12 +12,24 @@ class VoteService < BaseService @choices = choices @votes = [] - ApplicationRecord.transaction do - @choices.each do |choice| - @votes << @poll.votes.create!(account: @account, choice: choice) + already_voted = true + + RedisLock.acquire(lock_options) do |lock| + if lock.acquired? + already_voted = @poll.votes.where(account: @account).exists? + + ApplicationRecord.transaction do + @choices.each do |choice| + @votes << @poll.votes.create!(account: @account, choice: choice) + end + end + else + raise Mastodon::RaceConditionError end end + increment_voters_count! unless already_voted + ActivityTracker.increment('activity:interactions') if @poll.account.local? @@ -53,4 +65,18 @@ class VoteService < BaseService def build_json(vote) Oj.dump(serialize_payload(vote, ActivityPub::VoteSerializer)) end + + def increment_voters_count! + unless @poll.voters_count.nil? + @poll.voters_count = @poll.voters_count + 1 + @poll.save + end + rescue ActiveRecord::StaleObjectError + @poll.reload + retry + end + + def lock_options + { redis: Redis.current, key: "vote:#{@poll.id}:#{@account.id}" } + end end diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml index d6b36a5d1..d1aba6ef9 100644 --- a/app/views/statuses/_poll.html.haml +++ b/app/views/statuses/_poll.html.haml @@ -1,12 +1,13 @@ - show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired? - own_votes = user_signed_in? ? poll.own_votes(current_account) : [] +- total_votes_count = poll.voters_count || poll.votes_count .poll %ul - poll.loaded_options.each_with_index do |option, index| %li - if show_results - - percent = poll.votes_count > 0 ? 100 * option.votes_count / poll.votes_count : 0 + - percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0 %span.poll__chart{ style: "width: #{percent}%" } %label.poll__text>< @@ -24,7 +25,10 @@ %button.button.button-secondary{ disabled: true } = t('statuses.poll.vote') - %span= t('statuses.poll.total_votes', count: poll.votes_count) + - if poll.voters_count.nil? + %span= t('statuses.poll.total_votes', count: poll.votes_count) + - else + %span= t('statuses.poll.total_people', count: poll.voters_count) - unless poll.expires_at.nil? · diff --git a/config/locales/en.yml b/config/locales/en.yml index dbdfe0ca0..82e20cb1f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1030,6 +1030,9 @@ en: private: Non-public toot cannot be pinned reblog: A boost cannot be pinned poll: + total_people: + one: "%{count} person" + other: "%{count} people" total_votes: one: "%{count} vote" other: "%{count} votes" diff --git a/db/migrate/20190927232842_add_voters_count_to_polls.rb b/db/migrate/20190927232842_add_voters_count_to_polls.rb new file mode 100644 index 000000000..846385700 --- /dev/null +++ b/db/migrate/20190927232842_add_voters_count_to_polls.rb @@ -0,0 +1,5 @@ +class AddVotersCountToPolls < ActiveRecord::Migration[5.2] + def change + add_column :polls, :voters_count, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index 8eeaf48a0..557b777e0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_09_27_124642) do +ActiveRecord::Schema.define(version: 2019_09_27_232842) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -529,6 +529,7 @@ ActiveRecord::Schema.define(version: 2019_09_27_124642) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "lock_version", default: 0, null: false + t.bigint "voters_count" t.index ["account_id"], name: "index_polls_on_account_id" t.index ["status_id"], name: "index_polls_on_status_id" end -- cgit