diff --git a/app/admin/users.rb b/app/admin/users.rb index efe3b6558c1..376154ecaa1 100644 --- a/app/admin/users.rb +++ b/app/admin/users.rb @@ -122,7 +122,7 @@ def find_resource end panel("Memberships") do - table_for user.memberships.includes(:group, :user).order(:id).each do |m| + table_for user.memberships.includes(:group, :user, :revoker).order(:id).each do |m| column :id column :group_name do |g| group = g.group @@ -131,7 +131,10 @@ def find_resource column :volume column :admin column :accepted_at - column :archived_at + column :revoked_at + column :revoker do |m| + m.revoker.present? ? m.revoker.name : '' + end end end diff --git a/app/controllers/api/b3/users_controller.rb b/app/controllers/api/b3/users_controller.rb index 64d81f24363..c58c953b68a 100644 --- a/app/controllers/api/b3/users_controller.rb +++ b/app/controllers/api/b3/users_controller.rb @@ -9,8 +9,8 @@ def authenticate_api_key! end def deactivate - User.active.find(params[:id]) # throws 404 if not present - DeactivateUserWorker.perform_async(params[:id]) + user = User.active.find(params[:id]) # throws 404 if not present + DeactivateUserWorker.perform_async(user.id, user.id) render json: {success: :ok} end diff --git a/app/controllers/api/v1/profile_controller.rb b/app/controllers/api/v1/profile_controller.rb index e499eb320d8..18c6949d607 100644 --- a/app/controllers/api/v1/profile_controller.rb +++ b/app/controllers/api/v1/profile_controller.rb @@ -80,7 +80,7 @@ def avatar_uploaded end def destroy - service.deactivate(user: current_user) + service.deactivate(user: current_user, actor: current_user) respond_with_resource end diff --git a/app/extras/queries/users_by_volume_query.rb b/app/extras/queries/users_by_volume_query.rb index 9cbeb89437f..02d40418d57 100644 --- a/app/extras/queries/users_by_volume_query.rb +++ b/app/extras/queries/users_by_volume_query.rb @@ -25,7 +25,7 @@ def self.users_by_volume(model, operator, volume) joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{model.discussion_id || 0} AND dr.user_id = users.id"). joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{model.group_id || 0}"). joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{model.poll_id || 0} AND s.latest = TRUE"). - where('(m.id IS NOT NULL AND m.archived_at IS NULL) OR + where('(m.id IS NOT NULL AND m.revoked_at IS NULL) OR (dr.id IS NOT NULL and dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL) OR (s.id IS NOT NULL and s.revoked_at IS NULL AND s.inviter_id IS NOT NULL) OR (m.id IS NULL and dr.id IS NULL and s.id IS NULL)'). diff --git a/app/extras/queries/visible_autocompletes.rb b/app/extras/queries/visible_autocompletes.rb deleted file mode 100644 index 904c39ef492..00000000000 --- a/app/extras/queries/visible_autocompletes.rb +++ /dev/null @@ -1,43 +0,0 @@ -class Queries::VisibleAutocompletes < Delegator - - def initialize(query: , group: , pending: nil, limit: , offset: 0, current_user: ) - # want to match only first part of each word - # - # searching for 'rob' - # 'robguthrie' should be true - # 'emrob guthrie' should be false - # 'james robinson' should be true - # - @relation = if pending - Memberhship.pending - else - Membership.active - end - - @relation = @relation.joins(:user).joins(:group) - .where(group: group) - .where("users.id != ?", current_user.id) - .where("users.name ilike :qFirstWord OR - users.name ilike :qOtherWord OR - users.username ilike :qFirstWord", - qFirstWord: "#{query}%", - qOtherWord: "% #{query}%") - if query.to_s.length > 0 - @relation = @relation.order('users.name') - else - @relation = @relation.order('memberships.created_at desc') - end - - @relation = @relation.limit(limit).offset(offset) - super @relation - end - - def __getobj__ - @relation - end - - def __setobj__(obj) - @relation = obj - end - -end diff --git a/app/extras/queries/visible_invitable_memberships.rb b/app/extras/queries/visible_invitable_memberships.rb deleted file mode 100644 index c2aa7380ad2..00000000000 --- a/app/extras/queries/visible_invitable_memberships.rb +++ /dev/null @@ -1,44 +0,0 @@ -class Queries::VisibleInvitableMemberships < Delegator - - def initialize(user: nil, group: nil, query: nil, limit: nil) - @user, @group, @query, @limit = user, group, query, limit - @relation = visible_memberships_filtered - super @relation - end - - def __getobj__ - @relation - end - - def __setobj__(obj) - @relation = obj - end - - private - - def visible_memberships_filtered - Membership.select("DISTINCT ON (user_id) memberships.*") - .where(user_id: visible_user_ids, group_id: @user.group_ids, archived_at: nil) - .search_for(@query) - .limit(@limit) - end - - def visible_user_ids - Membership.connection.execute( - "SELECT DISTINCT memberships.user_id - FROM memberships - LEFT OUTER JOIN users u ON u.id = memberships.user_id AND memberships.group_id = #{@group.id} - WHERE memberships.group_id IN (#{@user.group_ids.join(', ')}) - AND u.id IS NULL - AND u.deactivated_at IS NULL").values.flatten.map(&:to_i) - end - - def group_membership_ids - @group_membership_ids ||= @group.membership_ids - end - - def search_term - @search_term ||= "'%#{@query}%'" - end - -end diff --git a/app/mailers/event_mailer.rb b/app/mailers/event_mailer.rb index 1a50d651bd9..3b08a8e2902 100644 --- a/app/mailers/event_mailer.rb +++ b/app/mailers/event_mailer.rb @@ -16,9 +16,18 @@ def event(recipient_id, event_id) @discussion = @event.eventable.discussion end - if @event.kind == "membership_created" && @event.eventable_type == "Group" - # if the membership has been deleted, let it go - return unless Membership.where(user_id: recipient_id, group_id: @event.eventable_id).exists? + if @event.eventable.respond_to?(:group_id) && @event.eventable.group_id + @membership = Membership.active.find_by( + group_id: @event.eventable.group_id, + user_id: recipient_id + ) + + # this might be necessary to comply with anti-spam rules + # if someone does not respond to the invitation, don't send them more emails + return if @membership && + !@recipient.email_verified && + !@membership.accepted_at && + !["membership_created", "membership_resent"].include?(@event.kind) end @utm_hash = { utm_medium: 'email', utm_campaign: @event.kind } diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 7c113fa541e..1557c9aa13d 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -88,7 +88,7 @@ def self.pg_search_insert_statement(id: nil, author_id: nil) has_many :items, -> { includes(:user) }, class_name: 'Event', dependent: :destroy has_many :discussion_readers, dependent: :destroy - has_many :readers,-> { merge DiscussionReader.not_revoked }, through: :discussion_readers, source: :user + has_many :readers,-> { merge DiscussionReader.active }, through: :discussion_readers, source: :user has_many :guests, -> { merge DiscussionReader.guests }, through: :discussion_readers, source: :user has_many :admin_guests, -> { merge DiscussionReader.admins }, through: :discussion_readers, source: :user include DiscussionExportRelations @@ -145,7 +145,7 @@ def members User.active. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.id || 0} AND dr.user_id = users.id"). joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}"). - where('(m.id IS NOT NULL AND m.archived_at IS NULL) OR + where('(m.id IS NOT NULL AND m.revoked_at IS NULL) OR (dr.id IS NOT NULL and dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL)') end @@ -153,7 +153,7 @@ def admins User.active. joins("LEFT OUTER JOIN discussion_readers dr ON dr.discussion_id = #{self.id || 0} AND dr.user_id = users.id"). joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}"). - where('(m.admin = TRUE AND m.id IS NOT NULL AND m.archived_at IS NULL) OR + where('(m.admin = TRUE AND m.id IS NOT NULL AND m.revoked_at IS NULL) OR (dr.admin = TRUE AND dr.id IS NOT NULL and dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL)') end diff --git a/app/models/discussion_reader.rb b/app/models/discussion_reader.rb index 11cafbeeb68..1e351ecf94e 100644 --- a/app/models/discussion_reader.rb +++ b/app/models/discussion_reader.rb @@ -12,9 +12,8 @@ class DiscussionReader < ApplicationRecord delegate :message_channel, to: :user scope :dangling, -> { joins('left join discussions on discussions.id = discussion_id left join users on users.id = user_id').where('discussions.id is null or users.id is null') } - # scope :spammy, -> { joins('left join users on users.id = user_id').where('users.email_verified = false') } - scope :not_revoked, -> { where("discussion_readers.revoked_at IS NULL") } + scope :active, -> { where("discussion_readers.revoked_at IS NULL") } scope :guests, -> { where("discussion_readers.inviter_id IS NOT NULL AND discussion_readers.revoked_at IS NULL") } diff --git a/app/models/group.rb b/app/models/group.rb index 9b549e0eed3..8a2db9f2ea6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -35,13 +35,13 @@ class Group < ApplicationRecord has_many :all_memberships, dependent: :destroy, class_name: 'Membership' has_many :all_members, through: :all_memberships, source: :user - has_many :memberships, -> { where archived_at: nil } + has_many :memberships, -> { active } has_many :members, through: :memberships, source: :user - has_many :accepted_memberships, -> { accepted }, class_name: 'Membership' + has_many :accepted_memberships, -> { active.accepted }, class_name: "Membership" has_many :accepted_members, through: :accepted_memberships, source: :user - has_many :admin_memberships, -> { where admin: true, archived_at: nil }, class_name: 'Membership' + has_many :admin_memberships, -> { active.where(admin: true) }, class_name: 'Membership' has_many :admins, through: :admin_memberships, source: :user has_many :membership_requests, dependent: :destroy @@ -271,18 +271,16 @@ def ensure_handle_is_not_empty def archive! Group.where(id: id_and_subgroup_ids).update_all(archived_at: DateTime.now) - Membership.where(group_id: id_and_subgroup_ids).update_all(archived_at: DateTime.now) reload end def unarchive! - Group.where(id: all_subgroup_ids.concat([id])).update_all(archived_at: nil) - Membership.where(group_id: all_subgroup_ids.concat([id])).update_all(archived_at: nil) + Group.where(id: id_and_subgroup_ids).update_all(archived_at: nil) reload end def org_memberships_count - Membership.not_archived.where(group_id: id_and_subgroup_ids).count('distinct user_id') + Membership.active.where(group_id: id_and_subgroup_ids).count('distinct user_id') end def org_members_count @@ -328,7 +326,7 @@ def full_name end def id_and_subgroup_ids - subgroup_ids.concat([id]).compact + subgroup_ids.concat([id]).compact.uniq end def identity_for(type) diff --git a/app/models/membership.rb b/app/models/membership.rb index b4acb37255d..64df70acb0b 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -23,14 +23,15 @@ def initialize(obj) belongs_to :group belongs_to :user belongs_to :inviter, class_name: 'User' + belongs_to :revoker, class_name: 'User' has_many :events, as: :eventable, dependent: :destroy scope :dangling, -> { joins('left join groups g on memberships.group_id = g.id').where('group_id is not null and g.id is null') } - scope :active, -> { not_archived.accepted } - scope :archived, -> { where('archived_at IS NOT NULL') } - scope :not_archived, -> { where(archived_at: nil) } - scope :pending, -> { where(accepted_at: nil).where("user_id is not null") } + scope :user_active, -> { joins(:user).where("users.deactivated_at is null") } + scope :active, -> { where(revoked_at: nil) } + scope :pending, -> { active.where(accepted_at: nil) } scope :accepted, -> { where('accepted_at IS NOT NULL') } + scope :revoked, -> { where('revoked_at IS NOT NULL') } scope :search_for, ->(query) { joins(:user).where("users.name ilike :query or users.username ilike :query or users.email ilike :query", query: "%#{query}%") } @@ -39,7 +40,7 @@ def initialize(obj) scope :for_group, lambda {|group| where(group_id: group)} scope :admin, -> { where(admin: true) } - has_paper_trail only: [:group_id, :user_id, :inviter_id, :admin, :title, :archived_at, :volume, :accepted_at] + has_paper_trail only: [:group_id, :user_id, :inviter_id, :admin, :title, :revoked_at, :revoker_id, :volume, :accepted_at] delegate :name, :email, to: :user, prefix: :user, allow_nil: true delegate :parent, to: :group, prefix: :group, allow_nil: true delegate :name, :full_name, to: :group, prefix: :group diff --git a/app/models/poll.rb b/app/models/poll.rb index afa4e6a208d..f46e928f090 100644 --- a/app/models/poll.rb +++ b/app/models/poll.rb @@ -393,7 +393,7 @@ def admins (p.author_id = users.id AND p.group_id IS NULL) OR (p.author_id = users.id AND dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL) OR (dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL AND dr.admin = TRUE) OR - (m.id IS NOT NULL AND m.archived_at IS NULL AND m.admin = TRUE) OR + (m.id IS NOT NULL AND m.revoked_at IS NULL AND m.admin = TRUE) OR (s.id IS NOT NULL AND s.revoked_at IS NULL AND latest = TRUE AND s.admin = TRUE)") end @@ -404,7 +404,7 @@ def members joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{self.group_id || 0}"). joins("LEFT OUTER JOIN stances s ON s.participant_id = users.id AND s.poll_id = #{self.id || 0}"). where("(dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL) OR - (m.id IS NOT NULL AND m.archived_at IS NULL) OR + (m.id IS NOT NULL AND m.revoked_at IS NULL) OR (s.id IS NOT NULL AND s.revoked_at IS NULL AND latest = TRUE)") end diff --git a/app/models/user.rb b/app/models/user.rb index 83f97326a48..b4cdbee6958 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -18,7 +18,7 @@ class User < ApplicationRecord extend NoSpam no_spam_for :name, :email - has_paper_trail only: [:email_newsletter] + has_paper_trail only: [:email_newsletter, :deactivated_at, :deactivator_id] MAX_AVATAR_IMAGE_SIZE_CONST = 100.megabytes BOT_EMAILS = { @@ -67,17 +67,9 @@ class User < ApplicationRecord -> { where('memberships.admin = ?', true) }, class_name: 'Membership' - has_many :memberships, -> { where(archived_at: nil) }, dependent: :destroy + has_many :memberships, -> { active }, dependent: :destroy has_many :all_memberships, dependent: :destroy, class_name: "Membership" - has_many :archived_memberships, - -> { where('archived_at IS NOT NULL') }, - class_name: 'Membership' - - has_many :invited_memberships, - class_name: 'Membership', - foreign_key: :inviter_id - has_many :groups, through: :memberships, class_name: 'Group', @@ -144,7 +136,7 @@ class User < ApplicationRecord active.verified.search_for(query). joins("LEFT OUTER JOIN memberships m ON m.user_id = users.id AND m.group_id = #{model.group_id || 0}"). joins("LEFT OUTER JOIN discussion_readers dr ON dr.user_id = users.id AND dr.discussion_id = #{model.discussion_id || 0}"). - where("(m.id IS NOT NULL AND m.archived_at IS NULL) OR + where("(m.id IS NOT NULL AND m.revoked_at IS NULL) OR (dr.id IS NOT NULL AND dr.inviter_id IS NOT NULL AND dr.revoked_at IS NULL)") end diff --git a/app/queries/poll_query.rb b/app/queries/poll_query.rb index 8b3e36ef92b..5006e8c705e 100644 --- a/app/queries/poll_query.rb +++ b/app/queries/poll_query.rb @@ -23,7 +23,7 @@ def self.visible_to(user: LoggedOutUser.new, .joins("LEFT OUTER JOIN stances s ON s.poll_id = polls.id AND (s.participant_id = #{user.id || 0} #{or_stance_token})") .where("#{'d.private = false OR ' if show_public} polls.author_id = :user_id OR - (m.id IS NOT NULL AND m.archived_at IS NULL) OR + (m.id IS NOT NULL AND m.revoked_at IS NULL) OR (dr.id IS NOT NULL AND dr.revoked_at IS NULL AND dr.inviter_id IS NOT NULL) OR (s.id IS NOT NULL AND s.revoked_at IS NULL)", user_id: user.id) chain diff --git a/app/queries/user_query.rb b/app/queries/user_query.rb index 0e81aa152c7..9384a1d11d5 100644 --- a/app/queries/user_query.rb +++ b/app/queries/user_query.rb @@ -15,7 +15,7 @@ def self.relations(model:, actor:) end rels.push User.joins('LEFT OUTER JOIN memberships m ON m.user_id = users.id'). - where('(m.group_id IN (:group_ids))', {group_ids: group_ids}) + where('m.group_id IN (:group_ids) AND m.revoked_at IS NULL', {group_ids: group_ids}) # people who have requested membership rels.push User.joins('LEFT OUTER JOIN membership_requests mr ON mr.requestor_id = users.id'). @@ -55,19 +55,19 @@ def self.relations(model:, actor:) if model.discussion_id rels.push( User.joins('LEFT OUTER JOIN discussion_readers dr ON dr.user_id = users.id'). - where('dr.discussion_id': model.discussion_id) + where('dr.discussion_id': model.discussion_id).where('dr.revoked_at IS NULL') ) rels.push( User.joins('LEFT OUTER JOIN stances ON stances.participant_id = users.id'). - where('stances.poll_id': model.discussion.poll_ids) + where('stances.poll_id': model.discussion.poll_ids).where("stances.revoked_at IS NULL") ) end if model.poll_id rels.push( User.joins('LEFT OUTER JOIN stances ON stances.participant_id = users.id'). - where('stances.poll_id': model.poll_id) + where('stances.poll_id': model.poll_id).where("stances.revoked_at IS NULL") ) end end diff --git a/app/services/announcement_service.rb b/app/services/announcement_service.rb index 202d16c5bc2..8b30401bf1b 100644 --- a/app/services/announcement_service.rb +++ b/app/services/announcement_service.rb @@ -7,8 +7,8 @@ def self.audience_users(model, kind, actor, exclude_members = false, include_act id = kind.match(/group-(\d+)/)[1].to_i group = model.group.parent_or_self.self_and_subgroups.find(id) raise CanCan::AccessDenied unless actor.can?(:notify, group) - group.accepted_members - when 'group' then model.group.accepted_members + group.members + when 'group' then model.group.members when 'discussion_group' then (model.discussion || NullDiscussion.new).readers when 'voters' then (model.poll || NullPoll.new).unmasked_voters when 'decided_voters' then (model.poll || NullPoll.new).unmasked_decided_voters diff --git a/app/services/group_service.rb b/app/services/group_service.rb index 0510d2a6d49..02e7caf8734 100644 --- a/app/services/group_service.rb +++ b/app/services/group_service.rb @@ -62,8 +62,7 @@ def self.invite(group:, params:, actor:) recipient_message: params[:recipient_message]) # EventBus.broadcast('group_invite', group, actor, all_memberships.size) - Membership.not_archived.where(group_id: group.id, user_id: users.pluck(:id)) - + Membership.active.where(group_id: group.id, user_id: users.pluck(:id)) end def self.create(group:, actor: ) diff --git a/app/services/record_cloner.rb b/app/services/record_cloner.rb index d037839f27e..2925c314b83 100644 --- a/app/services/record_cloner.rb +++ b/app/services/record_cloner.rb @@ -364,7 +364,8 @@ def new_clone_membership(membership) copy_fields = %w[ user_id inviter_id - archived_at + revoked_at + revoker_id admin volume experiences diff --git a/app/services/user_service.rb b/app/services/user_service.rb index 32c4ddf4e63..caebf3b1d11 100644 --- a/app/services/user_service.rb +++ b/app/services/user_service.rb @@ -32,15 +32,16 @@ def self.verify(user: ) # # it should, ideally, also send an undo link to the email address on file, # which is the only way for someone to claim this user account again - def self.deactivate(user:) - user.ability.authorize! :deactivate, user - DeactivateUserWorker.perform_async(user.id) + def self.deactivate(user:, actor:) + actor.ability.authorize! :deactivate, user + DeactivateUserWorker.perform_async(user.id, actor.id) end # this is for user accounts deactivated with the older method def self.reactivate(user_id) user = User.find(user_id) - Membership.where(user_id: user.id).update_all(archived_at: nil) + deactivated_at = user.deactivated_at + Membership.where(user_id: user.id, revoked_at: deactivated_at).update_all(revoked_at: nil) group_ids = Membership.where(user_id: user.id).pluck(:group_id) Group.where(id: group_ids).map(&:update_memberships_count) user.update(deactivated_at: nil) diff --git a/app/views/event_mailer/group.html.haml b/app/views/event_mailer/group.html.haml index 4a8c1ab244c..369ec0927d9 100644 --- a/app/views/event_mailer/group.html.haml +++ b/app/views/event_mailer/group.html.haml @@ -3,7 +3,7 @@ - membership = @event.eventable - else - group = @event.eventable - - membership = group.memberships.where(user_id: @recipient.id).first + - membership = group.all_memberships.where(user_id: @recipient.id).first = render "event_mailer/common/notification", with_title: true, event: @event, url: membership_url(membership) @@ -16,6 +16,10 @@ .text-center = render 'base_mailer/button', url: membership_url(membership), text: t(:"email.to_join_group.accept_invitation") -%p= t(:"email.loomio_app_description", site_name: AppConfig.theme[:site_name]) -= image_tag AppConfig.theme[:email_footer_logo_src], alt: "Logo", class: "thread-mailer__footer-logo" +- if !@recipient.email_verified + %p= t(:"email.to_join_group.accepting_is_important") + +.pt-4 + = image_tag AppConfig.theme[:email_footer_logo_src], alt: "#{AppConfig.theme[:site_name]} logo", class: "thread-mailer__footer-logo" + %p= t(:"email.loomio_app_description", site_name: AppConfig.theme[:site_name]) diff --git a/app/views/groups/stats.html.haml b/app/views/groups/stats.html.haml index 860366672ba..7ec4951fa40 100644 --- a/app/views/groups/stats.html.haml +++ b/app/views/groups/stats.html.haml @@ -24,7 +24,7 @@ - if @group.parent_or_self.id == ENV['SOLE_GROUP_ID'].to_i = @group.memberships_count - else - = @group.accepted_memberships_count + = @group.memberships_count %td %p{ style: "text-align: center"}= @group.subgroups.count %td diff --git a/app/workers/deactivate_user_worker.rb b/app/workers/deactivate_user_worker.rb index 67cb195b04e..8716c07c574 100644 --- a/app/workers/deactivate_user_worker.rb +++ b/app/workers/deactivate_user_worker.rb @@ -1,8 +1,9 @@ class DeactivateUserWorker include Sidekiq::Worker - def perform(user_id) + def perform(user_id, actor_id) user = User.find(user_id) + deactivated_at = DateTime.now User.transaction do @@ -10,37 +11,38 @@ def perform(user_id) email = user.email locale = user.locale - user.update(name: nil, - email: "deactivated-user-#{SecureRandom.uuid}@example.com", - short_bio: '', - username: nil, - avatar_kind: "initials", - avatar_initials: nil, - country: nil, - region: nil, - city: nil, - location: '', - email_newsletter: false, - unlock_token: nil, - current_sign_in_ip: nil, - last_sign_in_ip: nil, - encrypted_password: nil, - reset_password_token: nil, - reset_password_sent_at: nil, - unsubscribe_token: nil, - detected_locale: nil, - email_verified: false, - legal_accepted_at: false, - deactivated_at: Time.now) + user.update( + name: nil, + email: "deactivated-user-#{SecureRandom.uuid}@example.com", + short_bio: '', + username: nil, + avatar_kind: "initials", + avatar_initials: nil, + country: nil, + region: nil, + city: nil, + location: '', + email_newsletter: false, + unlock_token: nil, + current_sign_in_ip: nil, + last_sign_in_ip: nil, + encrypted_password: nil, + reset_password_token: nil, + reset_password_sent_at: nil, + unsubscribe_token: nil, + detected_locale: nil, + email_verified: false, + legal_accepted_at: false, + deactivated_at: deactivated_at, + deactivator_id: actor_id + ) UserMailer.deactivated(email, user.email, locale).deliver_later else - user.update(deactivated_at: Time.now) + user.update(deactivated_at: deactivated_at, deactivator_id: actor_id) end Identities::Base.where(user_id: user_id).delete_all - Membership.where(user_id: user_id).update_all(archived_at: Time.now) - group_ids = Membership.where(user_id: user_id).pluck(:group_id) Group.where(id: group_ids).map(&:update_memberships_count) diff --git a/app/workers/migrate_user_worker.rb b/app/workers/migrate_user_worker.rb index c03c1e312ee..8692817206b 100644 --- a/app/workers/migrate_user_worker.rb +++ b/app/workers/migrate_user_worker.rb @@ -6,12 +6,11 @@ class MigrateUserWorker def perform(source_id, destination_id) @source = User.find_by!(id: source_id) @destination = User.find_by!(id: destination_id) - unarchive_memberships(@source) delete_duplicates operations.each { |operation| ActiveRecord::Base.connection.execute(operation) } migrate_stances update_counters - DeactivateUserWorker.new.perform(source_id) + DeactivateUserWorker.new.perform(source_id, destination_id) UserMailer.accounts_merged(destination.id).deliver_later end @@ -35,10 +34,6 @@ def perform(source_id, destination_id) versions: :whodunnit }.freeze - def unarchive_memberships(user) - Membership.where(user_id: user.id).where('archived_at is not null').update_all(archived_at: nil) - end - def delete_duplicates Membership.delete(destination.all_memberships. joins("INNER JOIN memberships source diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index e8129c5c3ff..11d970e812c 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -274,6 +274,7 @@ ar: today: اليوم in_x_days: في٪ {x} يوم visit_loomio_help: قم بزيارة مساعدة Loomio + api_docs: مستندات API merge_accounts: modal: title: دمج الحسابات @@ -1283,6 +1284,8 @@ ar: help_translate: هل يمكنك مساعدتنا في ترجمة Loomio؟ updated_on_sign_in: يتم توفيره بواسطة المستعرض الخاص بك ويتم تحديثه عند تسجيل الدخول deactivated_user: مستخدم معطل + account_is_bot: هذا حساب بوت + bot_account_warning: لن تتم دعوة حسابات الروبوت للتصويت في استطلاعات الرأي أو المشاركة في المناقشات text_editor: insert_link: أدخل الرابط insert_embedded_url: تضمين الفيديو @@ -1544,6 +1547,7 @@ ar: this_is_for: هذا مكان رائع للمبيعات أو التدريب أو استفسارات الاشتراك أو لطلب الدعم باستخدام Loomio. read_the_manual: هل تعلم أن دليل المساعدة الخاص بنا يحتوي على أسئلة وأجوبة بالإضافة إلى أدلة مفصلة لتسهيل المناقشات والقرارات بشأن Loomio؟ success_via_email: شكرًا على رسالتك ، {{name}}. سنرد عبر البريد الإلكتروني في غضون 24 ساعة. + need_message: برجاء كتابة رسالة حتى نتمكن من مساعدتك :-) explore_page: header: استكشف المجموعات search_placeholder: ابحث عن المجموعات العامة @@ -1716,6 +1720,7 @@ ar: invited_by_name: بدعوة من {{اسم}} accepted: أعضاء show_users_in_subgroups: اعرض المستخدمين في مجموعات فرعية أيضًا + bot: بوت polls_panel: new_poll: استطلاع جديد any_type: استطلاع جديد @@ -2267,6 +2272,7 @@ ar: show_poll: تظهر الاستطلاع read_memberships: قراءة العضويات manage_memberships: إدارة العضويات (إضافة أو إزالة أعضاء المجموعة) + deprecated: تم إهمال واجهة برمجة التطبيقات هذه. يرجى الانتقال إلى APIv2 (راجع مستندات API في قائمة إعدادات مجموعتك) powered_by: slogan: قرارات أفضل معًا powered_by_loomio: مدعوم من Loomio diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 2b9ef8ca8d3..6d918a3d043 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -312,6 +312,7 @@ de: check_for_errors_and_try_again: Bitte überprüfen Sie das Formular auf Fehler und versuchen Sie es erneut minutes: Protokoll visit_loomio_help: Besuchen Sie die Loomio-Hilfe + api_docs: API-Dokumente merge_accounts: modal: title: Benutzerkonten zusammenführen @@ -1323,6 +1324,8 @@ de: help_translate: Können Sie uns bei der Übersetzung von Loomio helfen? updated_on_sign_in: Wird von Ihrem Browser bereitgestellt und aktualisiert, wenn Sie sich anmelden deactivated_user: Deaktivierter Benutzer + account_is_bot: Dies ist ein Bot-Konto + bot_account_warning: Bot-Konten werden nicht eingeladen, an Umfragen teilzunehmen oder an Diskussionen teilzunehmen text_editor: insert_link: Link einfügen insert_embedded_url: Video einfügen @@ -1584,6 +1587,7 @@ de: this_is_for: Dies ist ein großartiger Ort für Verkaufs-, Schulungs- oder Abonnementanfragen oder um Unterstützung bei der Verwendung von Loomio anzufordern. read_the_manual: Wussten Sie, dass unser Hilfehandbuch häufig gestellte Fragen sowie detaillierte Anleitungen zur Erleichterung von Diskussionen und Entscheidungen zu Loomio enthält? success_via_email: Vielen Dank für Ihre Nachricht, {{name}}. Wir antworten innerhalb von 24 Stunden per E-Mail. + need_message: Bitte geben Sie eine Nachricht ein, damit wir Ihnen helfen können :-) explore_page: header: Gruppen entdecken search_placeholder: Nach öffentlichen Gruppen suchen @@ -1756,6 +1760,7 @@ de: last_seen: Zuletzt gesehen am {{date}} invited_by_name: Eingeladen von {{name}} accepted: Mitglieder + bot: Bot polls_panel: new_poll: Neue Umfrage any_type: Alle Typen @@ -2307,6 +2312,7 @@ de: edit_api_key: API-Schlüssel bearbeiten subtitle: Stellen Sie über unsere API eine Verbindung zu Loomio und den übrigen Tools her save_to_show_docs: Kehren Sie nach dem Speichern hierher zurück, um Ihren API-Schlüssel zu finden + deprecated: Diese API ist veraltet. Bitte wechseln Sie zu APIv2 (siehe API-Dokumente in Ihrem Gruppeneinstellungsmenü). powered_by: slogan: Bessere Entscheidungen gemeinsam treffen powered_by_loomio: Powered by Loomio diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 9b68b6c3dc4..b15e52d3e1b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -820,7 +820,7 @@ en: poll_reminder: '{{name}} reminded {{length}} people to vote:' audiences: - group: Members of {{name}} + group: "{{name}}" discussion_group: Everyone in the thread voters: Everyone invited to vote decided_voters: Everyone who voted diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index aaa42912c10..c90576cb35f 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -312,6 +312,7 @@ es: prefix_eg: por ejemplo, {{val}} are_you_sure: "¿Está seguro?" visit_loomio_help: Visita la ayuda de Loomio + api_docs: Documentos API merge_accounts: modal: title: Fusionar cuentas @@ -1350,6 +1351,8 @@ es: help_translate: "¿Puedes ayudarnos a traducir Loomio?" updated_on_sign_in: Proporcionado por su navegador y actualizado al iniciar sesión deactivated_user: Usuario desactivado + account_is_bot: Esta es una cuenta de bot. + bot_account_warning: Las cuentas de bot no serán invitadas a votar en encuestas ni a participar en debates. text_editor: x_of_y_characters: "{{x}} / {{y}} caractéres" select_text_to_link: Selecciona el texto del enlace e inténtalo de nuevo @@ -1618,6 +1621,7 @@ es: Esperamos responder todos nuestros mensajes en el espacio de 24 horas. success: Gracias por tu mensaje, {{name}}. Nos pondremos en contacto contigo. success_via_email: Gracias por tu mensaje, {{name}}. Responderemos por email en el plazo de 24 horas. + need_message: Por favor escribe un mensaje para que podamos ayudarte :-) explore_page: header: Explorar grupos search_placeholder: Buscar grupos públicos @@ -1800,6 +1804,7 @@ es: last_seen: Último visto {{date}} invited_by_name: Invitado por {{name}} accepted: Integrantes + bot: Bot polls_panel: new_poll: Nuevo sondeo any_type: Todos los tipos @@ -2400,6 +2405,7 @@ es: read_memberships: leer membresías manage_memberships: administrar pertenencias (agregar o quitar miembros del grupo) include_body_label: Incluye el cuerpo del mensaje con notificación + deprecated: Esta API está en desuso. Pase a APIv2 (consulte los documentos de API en el menú de configuración de su grupo) powered_by: this_is_loomio_md: Loomio es una aplicación para tomar decisiones juntos. [Pruébalo gratis] (https://www.loomio.com) slogan: Mejor decisiones juntos diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 2110b11b1f7..6c36d2163ee 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -314,6 +314,7 @@ fr: prefix_eg: par exemple {{val}} are_you_sure: Es-tu sûr? visit_loomio_help: Visitez l'aide de Loomio + api_docs: Documents sur l'API merge_accounts: modal: title: Fusionner les comptes @@ -1354,6 +1355,8 @@ fr: help_translate: Pouvez vous nous aider à traduire Loomio? updated_on_sign_in: Fourni par votre navigateur et mis à jour lorsque vous vous connectez deactivated_user: Utilisateur désactivé + account_is_bot: Ceci est un compte de robot + bot_account_warning: Les comptes de robots ne seront pas invités à voter dans les sondages ou à participer aux discussions text_editor: x_of_y_characters: "{{x}} / {{y}} caractères" select_text_to_link: Sélectionnez le texte à lier et réessayez @@ -1622,6 +1625,7 @@ fr: Notre objectif est de répondre à tous vos messages en moins de 24 heures. success: Merci pour votre message, {{name}}. Nous prendrons rapidement contact. success_via_email: Merci pour votre message, {{name}}. Nous vous répondrons par e-mail dans les 24 heures. + need_message: Veuillez entrer un message afin que nous puissions vous aider :-) explore_page: header: Explorer les groupes search_placeholder: Rechercher des groupes publics @@ -1804,6 +1808,7 @@ fr: last_seen: Vu en dernier le {{date}} invited_by_name: Invité par {{name}} accepted: Membres + bot: Bot polls_panel: new_poll: Nouveau sondage any_type: Tous types @@ -2406,6 +2411,7 @@ fr: read_memberships: lire les appartenances manage_memberships: gérer les appartenances (ajouter ou supprimer des membres du groupe) include_body_label: Inclure le corps du message dans la notification + deprecated: Cette API est obsolète. Veuillez passer à APIv2 (voir la documentation API dans le menu des paramètres de votre groupe) powered_by: this_is_loomio_md: Loomio est une application pour prendre des décisions ensemble. [Essayez gratuitement] (https://www.loomio.com) slogan: De meilleures décisions ensemble diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index b86f5761155..07ed1b9719f 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -308,6 +308,7 @@ hu: today: Ma in_x_days: "%{x} napon belül" visit_loomio_help: Látogassa meg a Loomio súgóját + api_docs: API-dokumentumok merge_accounts: modal: submit: Ellenőrzés @@ -1291,6 +1292,8 @@ hu: help_translate: Tudna segíteni a Loomio lefordításában? updated_on_sign_in: A böngésző biztosítja, és bejelentkezéskor frissül deactivated_user: Deaktivált felhasználó + account_is_bot: Ez egy bot fiók + bot_account_warning: A robotfiókokat nem hívják meg szavazásra vagy vitákban való részvételre text_editor: insert_link: Illessz be egy hivatkozást insert_embedded_url: Videó beágyazása @@ -1543,6 +1546,7 @@ hu: this_is_for: Ez egy nagyszerű hely értékesítési, képzési vagy előfizetési kérdésekhez, vagy támogatás kéréséhez a Loomio használatával. read_the_manual: Tudta, hogy a Súgó kézikönyvünk GYIK-eket, valamint részletes útmutatókat tartalmaz a Loomióval kapcsolatos viták és döntések megkönnyítéséhez? success_via_email: Köszönjük üzenetét, {{name}}. 24 órán belül e-mailben válaszolunk. + need_message: Írj üzenetet, hogy segíthessünk :-) explore_page: header: Fedezze fel a csoportokat search_placeholder: Keress nyilvános csoportokat @@ -1715,6 +1719,7 @@ hu: guest: Vendég last_seen: 'Utoljára látott: {{dátum}}' invited_by_name: 'Meghívta: {{name}}' + bot: Bot polls_panel: new_poll: Új szavazás any_type: Minden típus @@ -2236,6 +2241,7 @@ hu: read_memberships: tagságokat olvasni manage_memberships: tagságok kezelése (csoporttagok hozzáadása vagy eltávolítása) include_body_label: Az üzenet szövegrésze az értesítéshez + deprecated: Ez az API elavult. Kérjük, lépjen át APIv2-re (lásd az API-dokumentumokat a csoportbeállítások menüjében) powered_by: slogan: Jobb döntések közösen powered_by_loomio: A Loomio az oldal motorja diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 7739c5b66d5..c20ea89aed6 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -314,6 +314,7 @@ it: prefix_eg: ad esempio {{val}} are_you_sure: Sei sicuro? visit_loomio_help: Visita Loomio aiuto + api_docs: Documenti API merge_accounts: modal: title: Unisci account @@ -1327,6 +1328,8 @@ it: help_translate: Puoi aiutarci a tradurre Loomio? updated_on_sign_in: Fornito dal tuo browser e aggiornato quando accedi deactivated_user: Utente disattivato + account_is_bot: Questo è un account bot + bot_account_warning: Gli account bot non saranno invitati a votare nei sondaggi o a partecipare alle discussioni text_editor: x_of_y_characters: "{{x}} / {{y}} caratteri" insert_link: Inserisci collegamento @@ -1584,6 +1587,7 @@ it: consent_message: Contattandoci acconsenti al nostro personale di accedere al tuo account se necessario.
Il nostro team di supporto parla inglese e, se necessario, tradurrà il tuo messaggio con Google Translate.
Miriamo a rispondere a tutti i messaggi entro 24 ore. success: Grazie per il tuo messaggio, {{name}}. Avrai presto nostre notizie. success_via_email: Grazie per il tuo messaggio, {{name}}. Risponderemo via e-mail entro 24 ore. + need_message: Per favore inserisci un messaggio così possiamo aiutarti :-) explore_page: header: Esplora gruppi search_placeholder: Cerca tra i gruppi pubblici @@ -1752,6 +1756,7 @@ it: last_seen: Visto l'ultima volta {{date}} invited_by_name: Invitato da {{name}} accepted: Membri + bot: Bot polls_panel: new_poll: Nuova votazione any_type: Tutti i tipi @@ -2320,6 +2325,7 @@ it: name_placeholder: Assegna un nome a questa integrazione in modo da poterla identificare event_kind_helptext: Di quali eventi vuoi che il webhook riceva notifiche? permissions_explaination: Seleziona quali azioni possono essere eseguite da questa integrazione + deprecated: Questa API è deprecata. Passa all'APIv2 (vedi la documentazione API nel menu delle impostazioni del gruppo) powered_by: slogan: Migliora insieme le decisioni powered_by_loomio: Fatto con Loomio diff --git a/config/locales/client.nl_NL.yml b/config/locales/client.nl_NL.yml index 9012dae3056..db73f1fa5db 100644 --- a/config/locales/client.nl_NL.yml +++ b/config/locales/client.nl_NL.yml @@ -289,6 +289,7 @@ nl_NL: today: Vandaag in_x_days: over %{x} dagen visit_loomio_help: Bezoek Loomio-hulp + api_docs: API-documenten merge_accounts: modal: title: Accounts samenvoegen @@ -1298,6 +1299,8 @@ nl_NL: help_translate: Kun jij ons helpen met het vertalen van Loomio? updated_on_sign_in: Verstrekt door uw browser en bijgewerkt wanneer u zich aanmeldt deactivated_user: Gedeactiveerde gebruiker + account_is_bot: Dit is een botaccount + bot_account_warning: Botaccounts worden niet uitgenodigd om te stemmen in opiniepeilingen of deel te nemen aan discussies text_editor: insert_link: Link invoegen insert_embedded_url: Video integreren @@ -1559,6 +1562,7 @@ nl_NL: this_is_for: Dit is een geweldige plek voor vragen over verkoop, training of abonnementen, of om ondersteuning te vragen bij het gebruik van Loomio. read_the_manual: Wist je dat onze Help-handleiding veelgestelde vragen bevat, evenals gedetailleerde handleidingen om discussies en beslissingen over Loomio te vergemakkelijken? success_via_email: Bedankt voor je bericht, {{name}}. We reageren binnen 24 uur per e-mail. + need_message: Vul een bericht in, zodat we je kunnen helpen :-) explore_page: header: Verken groepen search_placeholder: Zoek naar openbare groepen @@ -1731,6 +1735,7 @@ nl_NL: invited_by_name: Uitgenodigd door {{name}} accepted: Leden show_users_in_subgroups: Toon ook gebruikers in subgroepen + bot: Bot polls_panel: new_poll: Nieuwe peiling any_type: Alle types @@ -2287,6 +2292,7 @@ nl_NL: name_placeholder: Geef deze integratie een naam zodat u deze kunt identificeren event_kind_helptext: Van welke gebeurtenissen wilt u dat de webhook op de hoogte wordt gebracht? permissions_explaination: Selecteer welke acties door deze integratie kunnen worden uitgevoerd + deprecated: Deze API is verouderd. Ga naar APIv2 (zie API-documenten in het menu met groepsinstellingen) powered_by: slogan: Samen betere beslissingen powered_by_loomio: Powered by Loomio diff --git a/config/locales/client.pl.yml b/config/locales/client.pl.yml index f84278160be..3347dd92755 100644 --- a/config/locales/client.pl.yml +++ b/config/locales/client.pl.yml @@ -306,6 +306,7 @@ pl: today: Dzisiaj in_x_days: za %{x} dni visit_loomio_help: Odwiedź Loomio pomocy + api_docs: Dokumentacja API merge_accounts: modal: title: Połącz konta @@ -1315,6 +1316,8 @@ pl: help_translate: Czy możesz pomóc nam przetłumaczyć Loomio? updated_on_sign_in: Dostarczane przez przeglądarkę i aktualizowane po zalogowaniu deactivated_user: Dezaktywowany użytkownik + account_is_bot: To jest konto bota + bot_account_warning: Konta botów nie będą zapraszane do głosowania w ankietach ani do udziału w dyskusjach text_editor: insert_link: Wstaw link insert_embedded_url: Wstaw wideo @@ -1576,6 +1579,7 @@ pl: this_is_for: To świetne miejsce na zapytania dotyczące sprzedaży, szkoleń, subskrypcji lub prośby o wsparcie przy użyciu Loomio. read_the_manual: Czy wiesz, że nasz Podręcznik pomocy zawiera najczęściej zadawane pytania, a także szczegółowe przewodniki ułatwiające dyskusje i podejmowanie decyzji w Loomio? success_via_email: Dziękujemy za wiadomość, {{imię}}. Odpowiemy e-mailem w ciągu 24 godzin. + need_message: Wpisz wiadomość, abyśmy mogli Ci pomóc :-) explore_page: header: Przeglądaj grupy search_placeholder: Wyszukaj grupy publiczne @@ -1748,6 +1752,7 @@ pl: invited_by_name: Zaproszony/a przez {{name}} accepted: Osoby show_users_in_subgroups: Pokaż także użytkowników w podgrupach + bot: Nerw polls_panel: new_poll: Nowe głosowanie any_type: Wszystkie rodzaje @@ -2304,6 +2309,7 @@ pl: show_poll: pokaż ankietę read_memberships: przeczytaj członkostwo manage_memberships: zarządzać członkostwem (dodawać lub usuwać członków grupy) + deprecated: Ten interfejs API jest przestarzały. Przejdź na APIv2 (zobacz dokumentację API w menu ustawień grupy) powered_by: slogan: Lepsze decyzje razem powered_by_loomio: Wspierany przez Loomio diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index d6e0b3e7973..a9b02193c0b 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -314,6 +314,7 @@ pt_BR: prefix_eg: por exemplo {{val}} are_you_sure: Tem certeza? visit_loomio_help: Visite a ajuda do Loomio + api_docs: Documentos da API merge_accounts: modal: title: Mesclar contas @@ -1336,6 +1337,8 @@ pt_BR: help_translate: Você pode nos ajudar a traduzir o Loomio? updated_on_sign_in: Fornecido pelo seu navegador e atualizado quando você faz login deactivated_user: Usuário desativado + account_is_bot: Esta é uma conta de bot + bot_account_warning: As contas de bot não serão convidadas a votar em enquetes ou participar de discussões text_editor: x_of_y_characters: "{{x}} / {{y}} caracteres" select_text_to_link: Selecione o texto para vincular e tente novamente @@ -1604,6 +1607,7 @@ pt_BR: Nossa meta é responder a todas as mensagens dentro de 24 horas. success: Obrigado por sua mensagem, {{name}}. Nós responderemos em breve. success_via_email: Obrigado pela sua mensagem, {{name}}. Responderemos por e-mail em até 24 horas. + need_message: Por favor, digite uma mensagem para que possamos ajudá-lo :-) explore_page: header: Explorar grupos search_placeholder: Buscar por grupos abertos @@ -1786,6 +1790,7 @@ pt_BR: last_seen: Visto por ultimo em {{date}} invited_by_name: Convidado por {{name}} accepted: Membros + bot: Robô polls_panel: new_poll: Nova enquete any_type: Todos tipos @@ -2388,6 +2393,7 @@ pt_BR: read_memberships: ler assinaturas manage_memberships: gerenciar associações (adicionar ou remover membros do grupo) include_body_label: Incluir a mensagem na notificação. + deprecated: Esta API está obsoleta. Mude para APIv2 (consulte a documentação da API no menu de configurações do seu grupo) powered_by: this_is_loomio_md: Loomio é um aplicativo para tomar decisões em conjunto. [Experimente gratuitamente](https://www.loomio.com) slogan: Decisões melhores juntos diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index f8c020605b3..b7f9bc264be 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -306,6 +306,7 @@ uk: please_try_again: Будь ласка спробуйте ще раз minutes: хвилин visit_loomio_help: Відвідайте довідку Loomio + api_docs: Документи API merge_accounts: modal: title: Об’єднати облікові записи @@ -1318,6 +1319,8 @@ uk: date_time_pref_label: Формат дати й часу help_translate: Чи можете ви допомогти нам перекласти Loomio? deactivated_user: Деактивований користувач + account_is_bot: Це обліковий запис бота + bot_account_warning: Облікові записи ботів не будуть запрошені голосувати в опитуваннях або брати участь в обговореннях text_editor: select_text_to_link: Виберіть текст для посилання та повторіть спробу insert_link: Додайте посилання @@ -1583,6 +1586,7 @@ uk: Ми прагнемо відповідати на всі повідомлення протягом 24 годин. success: Дякуємо за повідомлення, {{name}}. Ми дамо відповідь за деякий час. success_via_email: Дякуємо за повідомлення, {{name}}. Ми надамо відповідь поштою протягом 24 годин. + need_message: Будь ласка, введіть повідомлення, щоб ми могли вам допомогти :-) explore_page: header: Переглянути групи search_placeholder: Шукати загальнодоступні групи @@ -1755,6 +1759,7 @@ uk: last_seen: Востанне переглянуто {{date}} invited_by_name: Запрошено {{name}} accepted: Учасники + bot: Бот polls_panel: new_poll: Нове опитування any_type: Усі типи @@ -2326,6 +2331,7 @@ uk: edit_api_key: Редагувати ключ API subtitle: Підключіться до Loomio до решти ваших інструментів за допомогою нашого API save_to_show_docs: Після збереження поверніться сюди, щоб знайти свій ключ API + deprecated: Цей API застарів. Будь ласка, перейдіть до APIv2 (перегляньте документи API у меню налаштувань групи) powered_by: slogan: Кращі рішення приймаються разом powered_by_loomio: Працює на Loomio diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index cbf8db1b88b..c3910713d8f 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -179,6 +179,7 @@ ar: link_help: 'انقر على الرابط أدناه لقبول الدعوة:' accept_invitation: اقبل الدعوة stop_emails: التوقف عن تلقي رسائل البريد الإلكتروني من%{site_name} + accepting_is_important: من المهم قبول دعوتك، وإلا فلن تتلقى إشعارات حول القرارات التي تتعلق بك. resend_to_join_group: subject: "%{member} في انتظارك للانضمام إلى%{group_name} على%{site_name}" user_added_to_group: diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 4df1f0e6e26..34ac37dfdb4 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -200,6 +200,7 @@ de: link_help: 'Klicke auf den unteren Link, um die Einladung anzunehmen:' accept_invitation: Einladung annehmen stop_emails: Keine E-Mails mehr erhalten von %{site_name} + accepting_is_important: Es ist wichtig, dass Sie Ihre Einladung annehmen, da Sie sonst keine Benachrichtigungen über Entscheidungen erhalten, die Sie betreffen. resend_to_join_group: subject: "%{member} wartet darauf, dass du %{group_name} auf %{site_name} beitrittst" user_added_to_group: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 291851b095e..105ec7e8481 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -215,6 +215,7 @@ en: link_help: "Click the link below to accept the invitation:" accept_invitation: "Accept invitation" stop_emails: "Stop receiving emails from %{site_name}" + accepting_is_important: It's important to accept your invitation, otherwise you will not receive notifications about decisions that involve you. resend_to_join_group: subject: "%{member} is waiting for you to join %{group_name} on %{site_name}" user_added_to_group: diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 2aba23b5e13..b1e3d35861f 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -198,6 +198,7 @@ es: link_help: 'Haz clic en el enlace para unirte al grupo:' accept_invitation: "Aceptar invitación\n " stop_emails: Dejar de recibir emails de %{site_name} + accepting_is_important: Es importante aceptar tu invitación, de lo contrario no recibirás notificaciones sobre decisiones que te involucren. resend_to_join_group: subject: "%{member} está esperando que te sumes a %{group_name} en %{site_name}" user_added_to_group: diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index b775d91cfe1..b697a46a34b 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -198,6 +198,7 @@ fr: link_help: 'Cliquez sur le lien ci-dessous pour accepter l''invitation:' accept_invitation: Accepter l'invitation stop_emails: Cesser de recevoir des emails de %{site_name} + accepting_is_important: Il est important d'accepter votre invitation, sinon vous ne recevrez pas de notifications concernant les décisions qui vous concernent. resend_to_join_group: subject: "%{member} attend que vous rejoignez %{group_name} sur %{site_name}" user_added_to_group: diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml index 08c47d3a3e0..303b21919c6 100644 --- a/config/locales/server.hu.yml +++ b/config/locales/server.hu.yml @@ -152,6 +152,7 @@ hu: link_help: 'Kattints az alábbi hivatkozásra a meghívás elfogadásához:' accept_invitation: Meghívás elfogadása stop_emails: A továbbiakban nem kapsz majd e-mailt a(z) %{site_name}oldalról + accepting_is_important: Fontos, hogy fogadja el a meghívást, különben nem kap értesítést az Önt érintő döntésekről. resend_to_join_group: subject: "%{member}arra vár, hogy csatlakozz a(z)%{group_name}csoporthoz a(z) %{site_name}oldalon" user_added_to_group: diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 096e9498c02..fc1b0fa5d18 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -178,6 +178,7 @@ it: link_help: 'Clicca sul link qui sotto per accettare l''invito:' accept_invitation: Accetta l'invito stop_emails: Smetti di ricevere email da %{site_name} + accepting_is_important: È importante accettare il tuo invito, altrimenti non riceverai notifiche sulle decisioni che ti coinvolgono. unsubscribe: 'Disiscriviti o cambia le preferenze email:' unsubscribe_html: "%{link_text} per disiscriverti o cambiare le preferenze email." catch_up: diff --git a/config/locales/server.nl_NL.yml b/config/locales/server.nl_NL.yml index 78cee12dd37..cbe58356c0f 100644 --- a/config/locales/server.nl_NL.yml +++ b/config/locales/server.nl_NL.yml @@ -196,6 +196,7 @@ nl_NL: link_help: 'Klik op onderstaande link om de uitnodiging te accepteren:' accept_invitation: Uitnodiging accepteren stop_emails: Geen e-mail meer van %{site_name} + accepting_is_important: Het is belangrijk dat u uw uitnodiging accepteert, anders ontvangt u geen meldingen over beslissingen waarbij u betrokken bent. resend_to_join_group: subject: "%{member} wacht op je om lid te worden van %{group_name} op%{site_name}" user_added_to_group: diff --git a/config/locales/server.pl.yml b/config/locales/server.pl.yml index 004da8fde1b..5b2b545a19f 100644 --- a/config/locales/server.pl.yml +++ b/config/locales/server.pl.yml @@ -194,6 +194,7 @@ pl: link_help: 'Kliknij poniższy link, aby zaakceptować zaproszenie:' accept_invitation: Przyjmij zaproszenie stop_emails: Przestań otrzymywać wiadomości e-mail od %{site_name} + accepting_is_important: Ważne jest, aby zaakceptować zaproszenie, w przeciwnym razie nie będziesz otrzymywać powiadomień o decyzjach, które Cię dotyczą. resend_to_join_group: subject: "%{member}czeka, aż dołączysz do %{group_name} na %{site_name}" user_added_to_group: diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 0ca463f9966..b62b8ed6269 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -199,6 +199,7 @@ pt_BR: link_help: 'Clique no link abaixo para aceitar o convite:' accept_invitation: Aceitar convite stop_emails: Para de receber emails de %{site_name} + accepting_is_important: É importante aceitar seu convite, caso contrário você não receberá notificações sobre decisões que envolvem você. resend_to_join_group: subject: "%{member} está esperando você entrar em %{group_name} no %{site_name}" user_added_to_group: diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 37d8311c17d..9f87cd7138f 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -154,6 +154,7 @@ uk: link_help: 'Натисніть на посилання нижче щоб прийняти запрошення:' accept_invitation: Прийняти запрошення stop_emails: Не отримувати електронні листи від %{site_name} + accepting_is_important: Важливо прийняти ваше запрошення, інакше ви не отримуватимете сповіщень про рішення, які стосуються вас. resend_to_join_group: subject: "%{member} очікує вашого долучення до %{group_name} на %{site_name}" user_added_to_group: diff --git a/db/migrate/20230929005608_rename_memberships_archived_at_to_revoked_at.rb b/db/migrate/20230929005608_rename_memberships_archived_at_to_revoked_at.rb new file mode 100644 index 00000000000..018506e364e --- /dev/null +++ b/db/migrate/20230929005608_rename_memberships_archived_at_to_revoked_at.rb @@ -0,0 +1,6 @@ +class RenameMembershipsArchivedAtToRevokedAt < ActiveRecord::Migration[7.0] + def change + rename_column :memberships, :archived_at, :revoked_at + add_column :memberships, :revoker_id, :integer + end +end diff --git a/db/migrate/20230929025124_add_deactivator_id_to_users.rb b/db/migrate/20230929025124_add_deactivator_id_to_users.rb new file mode 100644 index 00000000000..b31578650cf --- /dev/null +++ b/db/migrate/20230929025124_add_deactivator_id_to_users.rb @@ -0,0 +1,5 @@ +class AddDeactivatorIdToUsers < ActiveRecord::Migration[7.0] + def change + add_column :users, :deactivator_id, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index d63fd6a96ca..732c9b13d38 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -52,6 +52,7 @@ t.string "checksum" t.datetime "created_at", precision: nil, null: false t.string "service_name", null: false + t.index ["id"], name: "active_storage_blobs_idx", unique: true t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end @@ -482,7 +483,7 @@ t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil t.integer "inviter_id" - t.datetime "archived_at", precision: nil + t.datetime "revoked_at", precision: nil t.integer "inbox_position", default: 0 t.boolean "admin", default: false, null: false t.integer "volume" @@ -492,6 +493,7 @@ t.datetime "accepted_at", precision: nil t.string "title" t.datetime "saml_session_expires_at", precision: nil + t.integer "revoker_id" t.index ["created_at"], name: "index_memberships_on_created_at" t.index ["group_id", "user_id"], name: "index_memberships_on_group_id_and_user_id", unique: true t.index ["inviter_id"], name: "index_memberships_on_inviter_id" @@ -969,6 +971,7 @@ t.jsonb "link_previews", default: [], null: false t.integer "email_catch_up_day" t.string "date_time_pref" + t.integer "deactivator_id" t.string "api_key" t.index ["api_key"], name: "index_users_on_api_key" t.index ["email"], name: "index_users_on_email", unique: true diff --git a/spec/controllers/api/b2/discussions_controller_spec.rb b/spec/controllers/api/b2/discussions_controller_spec.rb index 665e806156c..356cb0b259b 100644 --- a/spec/controllers/api/b2/discussions_controller_spec.rb +++ b/spec/controllers/api/b2/discussions_controller_spec.rb @@ -30,7 +30,7 @@ end it 'missing permission - archived' do - Membership.where(group_id: group.id, user_id: user.id).update(archived_at: Time.now) + Membership.where(group_id: group.id, user_id: user.id).update(revoked_at: Time.now) post :create, params: { title: 'test', group_id: group.id, api_key: user.api_key } expect(response.status).to eq 403 end diff --git a/spec/controllers/api/v1/announcements_controller_spec.rb b/spec/controllers/api/v1/announcements_controller_spec.rb index 88752cfe246..f2c8cfa0fc6 100644 --- a/spec/controllers/api/v1/announcements_controller_spec.rb +++ b/spec/controllers/api/v1/announcements_controller_spec.rb @@ -507,7 +507,7 @@ expect(email_user.notifications.count).to eq 1 expect(email_user.email_verified).to be false expect(email_user.memberships.pending.count).to eq 1 - expect(group.members).to include email_user + expect(group.all_members).to include email_user end it 'invite to multiple groups at once' do @@ -541,7 +541,7 @@ member.reload expect(member.notifications.count).to eq 1 expect(member.memberships.accepted.count).to eq 2 - expect(subgroup.accepted_members).to include member + expect(subgroup.members).to include member end it 'does not auto accept subgroup invitiations to not accepted members' do diff --git a/spec/controllers/api/v1/registrations_controller_spec.rb b/spec/controllers/api/v1/registrations_controller_spec.rb index c923aca3921..087f274c51a 100644 --- a/spec/controllers/api/v1/registrations_controller_spec.rb +++ b/spec/controllers/api/v1/registrations_controller_spec.rb @@ -11,7 +11,7 @@ describe 'create' do let(:login_token) { create :login_token, user: User.create(email: registration_params[:email], email_verified: false) } - let(:pending_membership) { create :membership, user: User.create(email: registration_params[:email], email_verified: false) } + let(:pending_membership) { create :membership, accepted_at: nil, user: User.create(email: registration_params[:email], email_verified: false) } let(:pending_identity) { create :facebook_identity, email: registration_params[:email] } it 'creates a new user' do diff --git a/spec/controllers/memberships_controller_spec.rb b/spec/controllers/memberships_controller_spec.rb index 616e804f21b..028b0660394 100644 --- a/spec/controllers/memberships_controller_spec.rb +++ b/spec/controllers/memberships_controller_spec.rb @@ -7,7 +7,7 @@ let(:another_user) { FactoryBot.create(:user) } describe 'redeem' do - let(:membership) { create :membership, group: group } + let(:membership) { create :membership, group: group, accepted_at: nil } before do session[:pending_membership_token] = membership.token @@ -28,7 +28,7 @@ it "multiple pending memberships - same org same user" do subgroup = create(:group, parent: membership.group) - subgroup_membership = create(:membership, group: subgroup, user: membership.user) + subgroup_membership = create(:membership, group: subgroup, user: membership.user, accepted_at: nil) expect {get :consume}.to change { Event.count }.by(1) expect(response.status).to eq 200 membership.reload @@ -67,7 +67,7 @@ end describe "GET 'show'" do - let(:membership) { create(:membership, token: 'abc', group: group, user: user) } + let(:membership) { create(:membership, accepted_at: nil, token: 'abc', group: group, user: user) } context 'membership not found' do it 'renders error page with not found message' do diff --git a/spec/extras/queries/users_by_volume_query_spec.rb b/spec/extras/queries/users_by_volume_query_spec.rb index b8c792f9104..608d7b56aef 100644 --- a/spec/extras/queries/users_by_volume_query_spec.rb +++ b/spec/extras/queries/users_by_volume_query_spec.rb @@ -9,7 +9,7 @@ let(:user_with_membership_volume_quiet) { FactoryBot.create :user } let(:user_with_reader_volume_mute) { FactoryBot.create :user } let(:user_with_membership_volume_mute) { FactoryBot.create :user } - let(:user_with_archived_membership) { FactoryBot.create :user } + let(:user_with_revoked_membership) { FactoryBot.create :user } let(:discussion) { FactoryBot.create :discussion } @@ -18,8 +18,8 @@ discussion.group.add_member!(user_with_membership_volume_normal).set_volume! :normal discussion.group.add_member!(user_with_membership_volume_quiet).set_volume! :quiet discussion.group.add_member!(user_with_membership_volume_mute).set_volume! :mute - discussion.group.add_member!(user_with_archived_membership).set_volume! :normal - discussion.group.membership_for(user_with_archived_membership).update(archived_at: 1.day.ago) + discussion.group.add_member!(user_with_revoked_membership).set_volume! :normal + discussion.group.membership_for(user_with_revoked_membership).update(revoked_at: 1.day.ago) discussion.group.add_member!(user_with_reader_volume_loud).set_volume! :mute discussion.group.add_member!(user_with_reader_volume_normal).set_volume! :mute @@ -42,7 +42,7 @@ users.should_not include user_with_reader_volume_normal users.should_not include user_with_reader_volume_quiet users.should_not include user_with_reader_volume_mute - users.should_not include user_with_archived_membership + users.should_not include user_with_revoked_membership end it "normal or loud" do @@ -56,7 +56,7 @@ users.should_not include user_with_membership_volume_mute users.should_not include user_with_reader_volume_quiet users.should_not include user_with_reader_volume_mute - users.should_not include user_with_archived_membership + users.should_not include user_with_revoked_membership end it "mute" do @@ -70,7 +70,7 @@ users.should_not include user_with_membership_volume_quiet users.should_not include user_with_reader_volume_normal users.should_not include user_with_reader_volume_quiet - users.should_not include user_with_archived_membership + users.should_not include user_with_revoked_membership end it 'accepts a group' do @@ -84,7 +84,7 @@ users.should_not include user_with_reader_volume_mute users.should_not include user_with_membership_volume_quiet users.should_not include user_with_membership_volume_mute - users.should_not include user_with_archived_membership + users.should_not include user_with_revoked_membership end it 'deals with nils' do diff --git a/spec/factories.rb b/spec/factories.rb index 454ad60b6e6..c3eb86a8731 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -7,6 +7,7 @@ factory :membership do |m| m.user { |u| u.association(:user)} m.group { |g| g.association(:group)} + accepted_at { 1.day.ago } end factory :pending_membership, class: Membership do |m| diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 9dbd93e52de..5d24a42db6c 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -11,7 +11,7 @@ subject { ability } let(:own_pending_membership) { - create :membership, user: create(:user, email: "i@i.com", email_verified: false), group: group, inviter: user + create :membership, user: create(:user, email: "i@i.com", email_verified: false), group: group, inviter: user, accepted_at: nil } let(:other_members_pending_membership) { create :membership, user: create(:user, email: "h@h.com", email_verified: false), group: group, inviter: other_user @@ -242,7 +242,6 @@ it { should_not be_able_to(:make_admin, @other_membership) } it { should_not be_able_to(:destroy, @other_membership) } it { should be_able_to(:destroy, @membership) } - it { should be_able_to(:destroy, own_pending_membership) } it { should_not be_able_to(:destroy, other_members_pending_membership) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 28d30927540..bedb34cd650 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -110,10 +110,6 @@ it 'sets archived_at on the group' do group.archived_at.should be_present end - - it 'archives the memberships of the group' do - group.memberships.reload.all?{|m| m.reload.archived_at.should be_present} - end end describe '#unarchive!' do @@ -122,11 +118,7 @@ end it 'restores archived_at to nil on the group' do - group.archived_at.should be_nil - end - - it 'restores the memberships of the group' do - group.memberships.all?{|m| m.archived_at.should be_nil} + group.reload.archived_at.should be_nil end end end diff --git a/spec/services/membership_service_spec.rb b/spec/services/membership_service_spec.rb index c3ed7a52778..febac2b1755 100644 --- a/spec/services/membership_service_spec.rb +++ b/spec/services/membership_service_spec.rb @@ -5,7 +5,7 @@ let(:user) { create :user } let(:admin) { create :user } let(:unverified_user) { create :user, email_verified: false } - let(:membership) { create :membership, group: group, user: unverified_user } + let(:membership) { create :membership, group: group, user: unverified_user, accepted_at: nil } before { group.add_admin! admin } @@ -67,7 +67,7 @@ describe 'with alien group' do let!(:alien_group) { create :group } - let(:membership) { create :membership, group: group, inviter: admin, user: user, experiences: { invited_group_ids: [alien_group.id] } } + let(:membership) { create :membership, group: group, inviter: admin, user: user, experiences: { invited_group_ids: [alien_group.id] }, accepted_at: nil } before do MembershipService.redeem(membership: membership, actor: user) diff --git a/spec/services/user_service_spec.rb b/spec/services/user_service_spec.rb index 7b09cf8d6fc..7a395b25cb1 100644 --- a/spec/services/user_service_spec.rb +++ b/spec/services/user_service_spec.rb @@ -10,13 +10,13 @@ end it "deactivates the user" do - zombie = UserService.deactivate(user: @user) - expect(@membership.reload.archived_at).to be_present + zombie = UserService.deactivate(user: @user, actor: @user) + expect(@user.reload.deactivated_at).to be_present end it "changes their email address" do ENV['SCRUB_USER_DEACTIVATE'] = '1' - UserService.deactivate(user: @user) + UserService.deactivate(user: @user, actor: @user) @user.reload expect(@user.email).to match /deactivated-user-.+@example.com/ end @@ -24,7 +24,7 @@ it "changes their email address" do ENV['SCRUB_USER_DEACTIVATE'] = nil email = @user.email - UserService.deactivate(user: @user) + UserService.deactivate(user: @user, actor: @user) @user.reload expect(@user.email).to eq email end diff --git a/vue/src/components/common/recipients_autocomplete.vue b/vue/src/components/common/recipients_autocomplete.vue index c3a699fafd9..8c8c0d31374 100644 --- a/vue/src/components/common/recipients_autocomplete.vue +++ b/vue/src/components/common/recipients_autocomplete.vue @@ -14,7 +14,6 @@ export default } props: - autofocus: Boolean label: String placeholder: String hint: String @@ -210,7 +209,7 @@ export default ret.push id: 'group' name: @$t('announcement.audiences.group', name: @model.group().name) - size: @model.group().acceptedMembershipsCount + size: @model.group().membershipsCount icon: 'mdi-account-group' when 'discussion_group' ret.push @@ -250,11 +249,11 @@ export default [] groups.filter(AbilityService.canNotifyGroup).forEach (group) => - if group.acceptedMembershipsCount + if group.membershipsCount ret.push id: "group-#{group.id}" name: @$t('announcement.audiences.group', name: group.name) - size: group.acceptedMembershipsCount + size: group.membershipsCount icon: 'mdi-forum' ret.filter (a) =>