Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
		
						commit
						8fef96cbf5
					
				
					 18 changed files with 215 additions and 385 deletions
				
			
		|  | @ -139,9 +139,10 @@ jobs: | |||
|     docker: | ||||
|       - image: circleci/ruby:2.7-buster-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|       - image: circleci/postgres:12.2 | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|           POSTGRES_HOST_AUTH_METHOD: trust | ||||
|       - image: circleci/redis:5-alpine | ||||
|     steps: | ||||
|       - *attach_workspace | ||||
|  | @ -158,9 +159,10 @@ jobs: | |||
|     docker: | ||||
|       - image: circleci/ruby:2.7-buster-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|       - image: circleci/postgres:12.2 | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|           POSTGRES_HOST_AUTH_METHOD: trust | ||||
|       - image: circleci/redis:5-alpine | ||||
|     <<: *test_steps | ||||
| 
 | ||||
|  | @ -169,9 +171,10 @@ jobs: | |||
|     docker: | ||||
|       - image: circleci/ruby:2.6-buster-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|       - image: circleci/postgres:12.2 | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|           POSTGRES_HOST_AUTH_METHOD: trust | ||||
|       - image: circleci/redis:5-alpine | ||||
|     <<: *test_steps | ||||
| 
 | ||||
|  | @ -180,9 +183,10 @@ jobs: | |||
|     docker: | ||||
|       - image: circleci/ruby:2.5-buster-node | ||||
|         environment: *ruby_environment | ||||
|       - image: circleci/postgres:10.6-alpine | ||||
|       - image: circleci/postgres:12.2 | ||||
|         environment: | ||||
|           POSTGRES_USER: root | ||||
|           POSTGRES_HOST_AUTH_METHOD: trust | ||||
|       - image: circleci/redis:5-alpine | ||||
|     <<: *test_steps | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							|  | @ -91,7 +91,7 @@ VAGRANTFILE_API_VERSION = "2" | |||
| 
 | ||||
| Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| | ||||
| 
 | ||||
|   config.vm.box = "ubuntu/xenial64" | ||||
|   config.vm.box = "ubuntu/bionic64" | ||||
| 
 | ||||
|   config.vm.provider :virtualbox do |vb| | ||||
|     vb.name = "mastodon" | ||||
|  |  | |||
|  | @ -2,8 +2,18 @@ | |||
| 
 | ||||
| module Admin | ||||
|   class ActionLogsController < BaseController | ||||
|     def index | ||||
|       @action_logs = Admin::ActionLog.page(params[:page]) | ||||
|     before_action :set_action_logs | ||||
| 
 | ||||
|     def index; end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def set_action_logs | ||||
|       @action_logs = Admin::ActionLogFilter.new(filter_params).results.page(params[:page]) | ||||
|     end | ||||
| 
 | ||||
|     def filter_params | ||||
|       params.slice(:page, *Admin::ActionLogFilter::KEYS).permit(:page, *Admin::ActionLogFilter::KEYS) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -9,79 +9,8 @@ module Admin::ActionLogsHelper | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def relevant_log_changes(log) | ||||
|     if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action) | ||||
|       log.recorded_changes.slice('domain') | ||||
|     elsif log.target_type == 'CustomEmoji' && log.action == :update | ||||
|       log.recorded_changes.slice('domain', 'visible_in_picker') | ||||
|     elsif log.target_type == 'User' && [:promote, :demote].include?(log.action) | ||||
|       log.recorded_changes.slice('moderator', 'admin') | ||||
|     elsif log.target_type == 'User' && [:change_email].include?(log.action) | ||||
|       log.recorded_changes.slice('email', 'unconfirmed_email') | ||||
|     elsif log.target_type == 'DomainBlock' | ||||
|       log.recorded_changes.slice('severity', 'reject_media') | ||||
|     elsif log.target_type == 'Status' && log.action == :update | ||||
|       log.recorded_changes.slice('sensitive') | ||||
|     elsif log.target_type == 'Announcement' && log.action == :update | ||||
|       log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day') | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def log_extra_attributes(hash) | ||||
|     safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ') | ||||
|   end | ||||
| 
 | ||||
|   def log_change(val) | ||||
|     return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array) | ||||
|     safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→') | ||||
|   end | ||||
| 
 | ||||
|   def icon_for_log(log) | ||||
|     case log.target_type | ||||
|     when 'Account', 'User' | ||||
|       'user' | ||||
|     when 'CustomEmoji' | ||||
|       'file' | ||||
|     when 'Report' | ||||
|       'flag' | ||||
|     when 'DomainBlock' | ||||
|       'lock' | ||||
|     when 'DomainAllow' | ||||
|       'plus-circle' | ||||
|     when 'EmailDomainBlock' | ||||
|       'envelope' | ||||
|     when 'Status' | ||||
|       'pencil' | ||||
|     when 'AccountWarning' | ||||
|       'warning' | ||||
|     when 'Announcement' | ||||
|       'bullhorn' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def class_for_log_icon(log) | ||||
|     case log.action | ||||
|     when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve | ||||
|       'positive' | ||||
|     when :create | ||||
|       opposite_verbs?(log) ? 'negative' : 'positive' | ||||
|     when :update, :reset_password, :disable_2fa, :memorialize, :change_email | ||||
|       'neutral' | ||||
|     when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen | ||||
|       'negative' | ||||
|     when :destroy | ||||
|       opposite_verbs?(log) ? 'positive' : 'negative' | ||||
|     else | ||||
|       '' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def opposite_verbs?(log) | ||||
|     %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type) | ||||
|   end | ||||
| 
 | ||||
|   def linkable_log_target(record) | ||||
|     case record.class.name | ||||
|     when 'Account' | ||||
|  | @ -99,7 +28,7 @@ module Admin::ActionLogsHelper | |||
|     when 'AccountWarning' | ||||
|       link_to record.target_account.acct, admin_account_path(record.target_account_id) | ||||
|     when 'Announcement' | ||||
|       link_to "##{record.id}", edit_admin_announcement_path(record.id) | ||||
|       link_to truncate(record.text), edit_admin_announcement_path(record.id) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | @ -118,7 +47,7 @@ module Admin::ActionLogsHelper | |||
|         I18n.t('admin.action_logs.deleted_status') | ||||
|       end | ||||
|     when 'Announcement' | ||||
|       "##{attributes['id']}" | ||||
|       truncate(attributes['text']) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ module Admin::FilterHelper | |||
|     InviteFilter::KEYS, | ||||
|     RelationshipFilter::KEYS, | ||||
|     AnnouncementFilter::KEYS, | ||||
|     Admin::ActionLogFilter::KEYS, | ||||
|   ].flatten.freeze | ||||
| 
 | ||||
|   def filter_link_to(text, link_to_params, link_class_params = link_to_params) | ||||
|  |  | |||
|  | @ -32,6 +32,10 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => { | |||
|   }); | ||||
| }); | ||||
| 
 | ||||
| delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => { | ||||
|   target.form.submit(); | ||||
| }); | ||||
| 
 | ||||
| const onDomainBlockSeverityChange = (target) => { | ||||
|   const rejectMediaDiv   = document.querySelector('.input.with_label.domain_block_reject_media'); | ||||
|   const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports'); | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ColumnBackButtonSlim from '../../components/column_back_button_slim'; | |||
| import AccountAuthorizeContainer from './containers/account_authorize_container'; | ||||
| import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; | ||||
| import ScrollableList from '../../components/scrollable_list'; | ||||
| import { me } from '../../initial_state'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, | ||||
|  | @ -19,6 +20,8 @@ const messages = defineMessages({ | |||
| const mapStateToProps = state => ({ | ||||
|   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), | ||||
|   hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']), | ||||
|   locked: !!state.getIn(['accounts', me, 'locked']), | ||||
|   domain: state.getIn(['meta', 'domain']), | ||||
| }); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
|  | @ -31,6 +34,8 @@ class FollowRequests extends ImmutablePureComponent { | |||
|     shouldUpdateScroll: PropTypes.func, | ||||
|     hasMore: PropTypes.bool, | ||||
|     accountIds: ImmutablePropTypes.list, | ||||
|     locked: PropTypes.bool, | ||||
|     domain: PropTypes.string, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|     multiColumn: PropTypes.bool, | ||||
|   }; | ||||
|  | @ -44,7 +49,7 @@ class FollowRequests extends ImmutablePureComponent { | |||
|   }, 300, { leading: true }); | ||||
| 
 | ||||
|   render () { | ||||
|     const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props; | ||||
|     const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain } = this.props; | ||||
| 
 | ||||
|     if (!accountIds) { | ||||
|       return ( | ||||
|  | @ -55,6 +60,15 @@ class FollowRequests extends ImmutablePureComponent { | |||
|     } | ||||
| 
 | ||||
|     const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />; | ||||
|     const unlockedPrependMessage = locked ? null : ( | ||||
|       <div className='follow_requests-unlocked_explanation'> | ||||
|         <FormattedMessage | ||||
|           id='follow_requests.unlocked_explanation' | ||||
|           defaultMessage='Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.' | ||||
|           values={{ domain: domain }} | ||||
|         /> | ||||
|       </div> | ||||
|     ); | ||||
| 
 | ||||
|     return ( | ||||
|       <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}> | ||||
|  | @ -66,6 +80,7 @@ class FollowRequests extends ImmutablePureComponent { | |||
|           shouldUpdateScroll={shouldUpdateScroll} | ||||
|           emptyMessage={emptyMessage} | ||||
|           bindToDocument={!multiColumn} | ||||
|           prepend={unlockedPrependMessage} | ||||
|         > | ||||
|           {accountIds.map(id => | ||||
|             <AccountAuthorizeContainer key={id} id={id} />, | ||||
|  |  | |||
|  | @ -1532,6 +1532,10 @@ | |||
|       { | ||||
|         "defaultMessage": "You don't have any follow requests yet. When you receive one, it will show up here.", | ||||
|         "id": "empty_column.follow_requests" | ||||
|       }, | ||||
|       { | ||||
|         "defaultMessage": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", | ||||
|         "id": "follow_requests.unlocked_explanation" | ||||
|       } | ||||
|     ], | ||||
|     "path": "app/javascript/mastodon/features/follow_requests/index.json" | ||||
|  | @ -2961,4 +2965,4 @@ | |||
|     ], | ||||
|     "path": "app/javascript/mastodon/features/video/index.json" | ||||
|   } | ||||
| ] | ||||
| ] | ||||
|  |  | |||
|  | @ -168,6 +168,7 @@ | |||
|   "errors.unexpected_crash.report_issue": "Report issue", | ||||
|   "follow_request.authorize": "Authorize", | ||||
|   "follow_request.reject": "Reject", | ||||
|   "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", | ||||
|   "getting_started.developers": "Developers", | ||||
|   "getting_started.directory": "Profile directory", | ||||
|   "getting_started.documentation": "Documentation", | ||||
|  |  | |||
|  | @ -418,6 +418,11 @@ body, | |||
|       } | ||||
|     } | ||||
| 
 | ||||
|     &--with-select strong { | ||||
|       display: block; | ||||
|       margin-bottom: 10px; | ||||
|     } | ||||
| 
 | ||||
|     a { | ||||
|       display: inline-block; | ||||
|       color: $darker-text-color; | ||||
|  | @ -583,19 +588,22 @@ body, | |||
| } | ||||
| 
 | ||||
| .log-entry { | ||||
|   margin-bottom: 20px; | ||||
|   line-height: 20px; | ||||
|   padding: 15px 0; | ||||
|   background: $ui-base-color; | ||||
|   border-bottom: 1px solid lighten($ui-base-color, 4%); | ||||
| 
 | ||||
|   &:last-child { | ||||
|     border-bottom: 0; | ||||
|   } | ||||
| 
 | ||||
|   &__header { | ||||
|     display: flex; | ||||
|     justify-content: flex-start; | ||||
|     align-items: center; | ||||
|     padding: 10px; | ||||
|     background: $ui-base-color; | ||||
|     color: $darker-text-color; | ||||
|     border-radius: 4px 4px 0 0; | ||||
|     font-size: 14px; | ||||
|     position: relative; | ||||
|     padding: 0 10px; | ||||
|   } | ||||
| 
 | ||||
|   &__avatar { | ||||
|  | @ -622,44 +630,6 @@ body, | |||
|     color: $dark-text-color; | ||||
|   } | ||||
| 
 | ||||
|   &__extras { | ||||
|     background: lighten($ui-base-color, 6%); | ||||
|     border-radius: 0 0 4px 4px; | ||||
|     padding: 10px; | ||||
|     color: $darker-text-color; | ||||
|     font-family: $font-monospace, monospace; | ||||
|     font-size: 12px; | ||||
|     word-wrap: break-word; | ||||
|     min-height: 20px; | ||||
|   } | ||||
| 
 | ||||
|   &__icon { | ||||
|     font-size: 28px; | ||||
|     margin-right: 10px; | ||||
|     color: $dark-text-color; | ||||
|   } | ||||
| 
 | ||||
|   &__icon__overlay { | ||||
|     position: absolute; | ||||
|     top: 10px; | ||||
|     right: 10px; | ||||
|     width: 10px; | ||||
|     height: 10px; | ||||
|     border-radius: 50%; | ||||
| 
 | ||||
|     &.positive { | ||||
|       background: $success-green; | ||||
|     } | ||||
| 
 | ||||
|     &.negative { | ||||
|       background: lighten($error-red, 12%); | ||||
|     } | ||||
| 
 | ||||
|     &.neutral { | ||||
|       background: $ui-highlight-color; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   a, | ||||
|   .username, | ||||
|   .target { | ||||
|  | @ -667,18 +637,6 @@ body, | |||
|     text-decoration: none; | ||||
|     font-weight: 500; | ||||
|   } | ||||
| 
 | ||||
|   .diff-old { | ||||
|     color: lighten($error-red, 12%); | ||||
|   } | ||||
| 
 | ||||
|   .diff-neutral { | ||||
|     color: $secondary-text-color; | ||||
|   } | ||||
| 
 | ||||
|   .diff-new { | ||||
|     color: $success-green; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| a.name-tag, | ||||
|  |  | |||
|  | @ -3800,7 +3800,8 @@ a.status-card.compact:hover { | |||
| } | ||||
| 
 | ||||
| .empty-column-indicator, | ||||
| .error-column { | ||||
| .error-column, | ||||
| .follow_requests-unlocked_explanation { | ||||
|   color: $dark-text-color; | ||||
|   background: $ui-base-color; | ||||
|   text-align: center; | ||||
|  | @ -3831,6 +3832,11 @@ a.status-card.compact:hover { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .follow_requests-unlocked_explanation { | ||||
|   background: darken($ui-base-color, 4%); | ||||
|   contain: initial; | ||||
| } | ||||
| 
 | ||||
| .error-column { | ||||
|   flex-direction: column; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										81
									
								
								app/models/admin/action_log_filter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								app/models/admin/action_log_filter.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Admin::ActionLogFilter | ||||
|   KEYS = %i( | ||||
|     action_type | ||||
|     account_id | ||||
|     target_account_id | ||||
|   ).freeze | ||||
| 
 | ||||
|   ACTION_TYPE_MAP = { | ||||
|     assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze, | ||||
|     change_email_user: { target_type: 'User', action: 'change_email' }.freeze, | ||||
|     confirm_user: { target_type: 'User', action: 'confirm' }.freeze, | ||||
|     create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze, | ||||
|     create_announcement: { target_type: 'Announcement', action: 'create' }.freeze, | ||||
|     create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze, | ||||
|     create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze, | ||||
|     create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze, | ||||
|     create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze, | ||||
|     demote_user: { target_type: 'User', action: 'demote' }.freeze, | ||||
|     destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze, | ||||
|     destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze, | ||||
|     destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze, | ||||
|     destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze, | ||||
|     destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze, | ||||
|     destroy_status: { target_type: 'Status', action: 'destroy' }.freeze, | ||||
|     disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze, | ||||
|     disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze, | ||||
|     disable_user: { target_type: 'User', action: 'disable' }.freeze, | ||||
|     enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze, | ||||
|     enable_user: { target_type: 'User', action: 'enable' }.freeze, | ||||
|     memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze, | ||||
|     promote_user: { target_type: 'User', action: 'promote' }.freeze, | ||||
|     remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze, | ||||
|     reopen_report: { target_type: 'Report', action: 'reopen' }.freeze, | ||||
|     reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze, | ||||
|     resolve_report: { target_type: 'Report', action: 'resolve' }.freeze, | ||||
|     silence_account: { target_type: 'Account', action: 'silence' }.freeze, | ||||
|     suspend_account: { target_type: 'Account', action: 'suspend' }.freeze, | ||||
|     unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze, | ||||
|     unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze, | ||||
|     unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze, | ||||
|     update_announcement: { target_type: 'Announcement', action: 'update' }.freeze, | ||||
|     update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze, | ||||
|     update_status: { target_type: 'Status', action: 'update' }.freeze, | ||||
|   }.freeze | ||||
| 
 | ||||
|   attr_reader :params | ||||
| 
 | ||||
|   def initialize(params) | ||||
|     @params = params | ||||
|   end | ||||
| 
 | ||||
|   def results | ||||
|     scope = Admin::ActionLog.includes(:target) | ||||
| 
 | ||||
|     params.each do |key, value| | ||||
|       next if key.to_s == 'page' | ||||
| 
 | ||||
|       scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present? | ||||
|     end | ||||
| 
 | ||||
|     scope | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def scope_for(key, value) | ||||
|     case key | ||||
|     when 'action_type' | ||||
|       Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym]) | ||||
|     when 'account_id' | ||||
|       Admin::ActionLog.where(account_id: value) | ||||
|     when 'target_account_id' | ||||
|       account = Account.find(value) | ||||
|       Admin::ActionLog.where(target: [account, account.user].compact) | ||||
|     else | ||||
|       raise "Unknown filter: #{key}" | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -53,7 +53,7 @@ | |||
|       .dashboard__counters__num= number_with_delimiter @account.targeted_reports.count | ||||
|       .dashboard__counters__label= t '.targeted_reports' | ||||
|   %div | ||||
|     %div | ||||
|     = link_to admin_action_logs_path(target_account_id: @account.id) do | ||||
|       .dashboard__counters__text | ||||
|         - if @account.local? && @account.user.nil? | ||||
|           %span.neutral= t('admin.accounts.deleted') | ||||
|  |  | |||
|  | @ -7,9 +7,3 @@ | |||
|         = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe | ||||
|       .log-entry__timestamp | ||||
|         %time.formatted{ datetime: action_log.created_at.iso8601 } | ||||
|     .spacer | ||||
|     .log-entry__icon | ||||
|       = fa_icon icon_for_log(action_log) | ||||
|       .log-entry__icon__overlay{ class: class_for_log_icon(action_log) } | ||||
|   .log-entry__extras | ||||
|     = log_extra_attributes relevant_log_changes(action_log) | ||||
|  |  | |||
|  | @ -1,6 +1,28 @@ | |||
| - content_for :page_title do | ||||
|   = t('admin.action_logs.title') | ||||
| 
 | ||||
| = render @action_logs | ||||
| - content_for :header_tags do | ||||
|   = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' | ||||
| 
 | ||||
| = form_tag admin_action_logs_url, method: 'GET', class: 'simple_form' do | ||||
|   = hidden_field_tag :target_account_id, params[:target_account_id] if params[:target_account_id].present? | ||||
| 
 | ||||
|   .filters | ||||
|     .filter-subset.filter-subset--with-select | ||||
|       %strong= t('admin.action_logs.filter_by_user') | ||||
|       .input.select.optional | ||||
|         = select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all') | ||||
| 
 | ||||
|     .filter-subset.filter-subset--with-select | ||||
|       %strong= t('admin.action_logs.filter_by_action') | ||||
|       .input.select.optional | ||||
|         = select_tag :action_type, options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key]}, params[:action_type]), prompt: I18n.t('admin.accounts.moderation.all') | ||||
| 
 | ||||
| - if @action_logs.empty? | ||||
|   %div.muted-hint.center-text | ||||
|     = t 'admin.action_logs.empty' | ||||
| - else | ||||
|   .announcements-list | ||||
|     = render @action_logs | ||||
| 
 | ||||
| = paginate @action_logs | ||||
|  |  | |||
|  | @ -2,6 +2,6 @@ | |||
| 
 | ||||
| Kaminari.configure do |config| | ||||
|   config.default_per_page = 40 | ||||
|   config.window = 1 | ||||
|   config.window = 2 | ||||
|   config.outer_window = 1 | ||||
| end | ||||
|  |  | |||
|  | @ -195,6 +195,42 @@ en: | |||
|       web: Web | ||||
|       whitelisted: Whitelisted | ||||
|     action_logs: | ||||
|       action_types: | ||||
|         assigned_to_self_report: Assign Report | ||||
|         change_email_user: Change E-mail for User | ||||
|         confirm_user: Confirm User | ||||
|         create_account_warning: Create Warning | ||||
|         create_announcement: Create Announcement | ||||
|         create_custom_emoji: Create Custom Emoji | ||||
|         create_domain_allow: Create Domain Allow | ||||
|         create_domain_block: Create Domain Block | ||||
|         create_email_domain_block: Create E-mail Domain Block | ||||
|         demote_user: Demote User | ||||
|         destroy_announcement: Delete Announcement | ||||
|         destroy_custom_emoji: Delete Custom Emoji | ||||
|         destroy_domain_allow: Delete Domain Allow | ||||
|         destroy_domain_block: Delete Domain Block | ||||
|         destroy_email_domain_block: Delete e-mail domain block | ||||
|         destroy_status: Delete Status | ||||
|         disable_2fa_user: Disable 2FA | ||||
|         disable_custom_emoji: Disable Custom Emoji | ||||
|         disable_user: Disable User | ||||
|         enable_custom_emoji: Enable Custom Emoji | ||||
|         enable_user: Enable User | ||||
|         memorialize_account: Memorialize Account | ||||
|         promote_user: Promote User | ||||
|         remove_avatar_user: Remove Avatar | ||||
|         reopen_report: Reopen Report | ||||
|         reset_password_user: Reset Password | ||||
|         resolve_report: Resolve Report | ||||
|         silence_account: Silence Account | ||||
|         suspend_account: Suspend Account | ||||
|         unassigned_report: Unassign Report | ||||
|         unsilence_account: Unsilence Account | ||||
|         unsuspend_account: Unsuspend Account | ||||
|         update_announcement: Update Announcement | ||||
|         update_custom_emoji: Update Custom Emoji | ||||
|         update_status: Update Status | ||||
|       actions: | ||||
|         assigned_to_self_report: "%{name} assigned report %{target} to themselves" | ||||
|         change_email_user: "%{name} changed the e-mail address of user %{target}" | ||||
|  | @ -232,6 +268,9 @@ en: | |||
|         update_custom_emoji: "%{name} updated emoji %{target}" | ||||
|         update_status: "%{name} updated status by %{target}" | ||||
|       deleted_status: "(deleted status)" | ||||
|       empty: No logs found. | ||||
|       filter_by_action: Filter by action | ||||
|       filter_by_user: Filter by user | ||||
|       title: Audit log | ||||
|     announcements: | ||||
|       destroyed_msg: Announcement successfully deleted! | ||||
|  |  | |||
|  | @ -31,242 +31,4 @@ RSpec.describe Admin::ActionLogsHelper, type: :helper do | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#relevant_log_changes' do | ||||
|     let(:log) { double(target_type: target_type, action: log_action, recorded_changes: recorded_changes) } | ||||
|     let(:recorded_changes) { double } | ||||
| 
 | ||||
|     after do | ||||
|       hoge.relevant_log_changes(log) | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)" do | ||||
|       let(:target_type) { 'CustomEmoji' } | ||||
|       let(:log_action)  { :enable } | ||||
| 
 | ||||
|       it "calls log.recorded_changes.slice('domain')" do | ||||
|         expect(recorded_changes).to receive(:slice).with('domain') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'CustomEmoji' && log.action == :update" do | ||||
|       let(:target_type) { 'CustomEmoji' } | ||||
|       let(:log_action)  { :update } | ||||
| 
 | ||||
|       it "calls log.recorded_changes.slice('domain', 'visible_in_picker')" do | ||||
|         expect(recorded_changes).to receive(:slice).with('domain', 'visible_in_picker') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'User' && [:promote, :demote].include?(log.action)" do | ||||
|       let(:target_type) { 'User' } | ||||
|       let(:log_action)  { :promote } | ||||
| 
 | ||||
|       it "calls log.recorded_changes.slice('moderator', 'admin')" do | ||||
|         expect(recorded_changes).to receive(:slice).with('moderator', 'admin') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'User' && [:change_email].include?(log.action)" do | ||||
|       let(:target_type) { 'User' } | ||||
|       let(:log_action)  { :change_email } | ||||
| 
 | ||||
|       it "calls log.recorded_changes.slice('email', 'unconfirmed_email')" do | ||||
|         expect(recorded_changes).to receive(:slice).with('email', 'unconfirmed_email') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'DomainBlock'" do | ||||
|       let(:target_type) { 'DomainBlock' } | ||||
|       let(:log_action)  { nil } | ||||
| 
 | ||||
|       it "calls log.recorded_changes.slice('severity', 'reject_media')" do | ||||
|         expect(recorded_changes).to receive(:slice).with('severity', 'reject_media') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'Status' && log.action == :update" do | ||||
|       let(:target_type) { 'Status' } | ||||
|       let(:log_action)  { :update } | ||||
| 
 | ||||
|       it "log.recorded_changes.slice('sensitive')" do | ||||
|         expect(recorded_changes).to receive(:slice).with('sensitive') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#log_extra_attributes' do | ||||
|     after do | ||||
|       hoge.log_extra_attributes(hoge: 'hoge') | ||||
|     end | ||||
| 
 | ||||
|     it "calls content_tag(:span, key, class: 'diff-key')" do | ||||
|       allow(hoge).to receive(:log_change).with(anything) | ||||
|       expect(hoge).to receive(:content_tag).with(:span, :hoge, class: 'diff-key') | ||||
|     end | ||||
| 
 | ||||
|     it 'calls safe_join twice' do | ||||
|       expect(hoge).to receive(:safe_join).with( | ||||
|         ['<span class="diff-key">hoge</span>', | ||||
|          '=', | ||||
|          '<span class="diff-neutral">hoge</span>'] | ||||
|       ) | ||||
| 
 | ||||
|       expect(hoge).to receive(:safe_join).with([nil], ' ') | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#log_change' do | ||||
|     after do | ||||
|       hoge.log_change(val) | ||||
|     end | ||||
| 
 | ||||
|     context '!val.is_a?(Array)' do | ||||
|       let(:val) { 'hoge' } | ||||
| 
 | ||||
|       it "calls content_tag(:span, val, class: 'diff-neutral')" do | ||||
|         expect(hoge).to receive(:content_tag).with(:span, val, class: 'diff-neutral') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'val.is_a?(Array)' do | ||||
|       let(:val) { %w(foo bar) } | ||||
| 
 | ||||
|       it 'calls #content_tag twice and #safe_join' do | ||||
|         expect(hoge).to receive(:content_tag).with(:span, 'foo', class: 'diff-old') | ||||
|         expect(hoge).to receive(:content_tag).with(:span, 'bar', class: 'diff-new') | ||||
|         expect(hoge).to receive(:safe_join).with([nil, nil], '→') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#icon_for_log' do | ||||
|     subject   { hoge.icon_for_log(log) } | ||||
| 
 | ||||
|     context "log.target_type == 'Account'" do | ||||
|       let(:log) { double(target_type: 'Account') } | ||||
| 
 | ||||
|       it 'returns "user"' do | ||||
|         expect(subject).to be 'user' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'User'" do | ||||
|       let(:log) { double(target_type: 'User') } | ||||
| 
 | ||||
|       it 'returns "user"' do | ||||
|         expect(subject).to be 'user' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'CustomEmoji'" do | ||||
|       let(:log) { double(target_type: 'CustomEmoji') } | ||||
| 
 | ||||
|       it 'returns "file"' do | ||||
|         expect(subject).to be 'file' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'Report'" do | ||||
|       let(:log) { double(target_type: 'Report') } | ||||
| 
 | ||||
|       it 'returns "flag"' do | ||||
|         expect(subject).to be 'flag' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'DomainBlock'" do | ||||
|       let(:log) { double(target_type: 'DomainBlock') } | ||||
| 
 | ||||
|       it 'returns "lock"' do | ||||
|         expect(subject).to be 'lock' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'EmailDomainBlock'" do | ||||
|       let(:log) { double(target_type: 'EmailDomainBlock') } | ||||
| 
 | ||||
|       it 'returns "envelope"' do | ||||
|         expect(subject).to be 'envelope' | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "log.target_type == 'Status'" do | ||||
|       let(:log) { double(target_type: 'Status') } | ||||
| 
 | ||||
|       it 'returns "pencil"' do | ||||
|         expect(subject).to be 'pencil' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#class_for_log_icon' do | ||||
|     subject   { hoge.class_for_log_icon(log) } | ||||
| 
 | ||||
|     %i(enable unsuspend unsilence confirm promote resolve).each do |action| | ||||
|       context "log.action == #{action}" do | ||||
|         let(:log) { double(action: action) } | ||||
| 
 | ||||
|         it 'returns "positive"' do | ||||
|           expect(subject).to be 'positive' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'log.action == :create' do | ||||
|       context 'opposite_verbs?(log)' do | ||||
|         let(:log) { double(action: :create, target_type: 'DomainBlock') } | ||||
| 
 | ||||
|         it 'returns "negative"' do | ||||
|           expect(subject).to be 'negative' | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context '!opposite_verbs?(log)' do | ||||
|         let(:log) { double(action: :create, target_type: '') } | ||||
| 
 | ||||
|         it 'returns "positive"' do | ||||
|           expect(subject).to be 'positive' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     %i(update reset_password disable_2fa memorialize change_email).each do |action| | ||||
|       context "log.action == #{action}" do | ||||
|         let(:log) { double(action: action) } | ||||
| 
 | ||||
|         it 'returns "neutral"' do | ||||
|           expect(subject).to be 'neutral' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     %i(demote silence disable suspend remove_avatar remove_header reopen).each do |action| | ||||
|       context "log.action == #{action}" do | ||||
|         let(:log) { double(action: action) } | ||||
| 
 | ||||
|         it 'returns "negative"' do | ||||
|           expect(subject).to be 'negative' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'log.action == :destroy' do | ||||
|       context 'opposite_verbs?(log)' do | ||||
|         let(:log) { double(action: :destroy, target_type: 'DomainBlock') } | ||||
| 
 | ||||
|         it 'returns "positive"' do | ||||
|           expect(subject).to be 'positive' | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context '!opposite_verbs?(log)' do | ||||
|         let(:log) { double(action: :destroy, target_type: '') } | ||||
| 
 | ||||
|         it 'returns "negative"' do | ||||
|           expect(subject).to be 'negative' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue