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: |     docker: | ||||||
|       - image: circleci/ruby:2.7-buster-node |       - image: circleci/ruby:2.7-buster-node | ||||||
|         environment: *ruby_environment |         environment: *ruby_environment | ||||||
|       - image: circleci/postgres:10.6-alpine |       - image: circleci/postgres:12.2 | ||||||
|         environment: |         environment: | ||||||
|           POSTGRES_USER: root |           POSTGRES_USER: root | ||||||
|  |           POSTGRES_HOST_AUTH_METHOD: trust | ||||||
|       - image: circleci/redis:5-alpine |       - image: circleci/redis:5-alpine | ||||||
|     steps: |     steps: | ||||||
|       - *attach_workspace |       - *attach_workspace | ||||||
|  | @ -158,9 +159,10 @@ jobs: | ||||||
|     docker: |     docker: | ||||||
|       - image: circleci/ruby:2.7-buster-node |       - image: circleci/ruby:2.7-buster-node | ||||||
|         environment: *ruby_environment |         environment: *ruby_environment | ||||||
|       - image: circleci/postgres:10.6-alpine |       - image: circleci/postgres:12.2 | ||||||
|         environment: |         environment: | ||||||
|           POSTGRES_USER: root |           POSTGRES_USER: root | ||||||
|  |           POSTGRES_HOST_AUTH_METHOD: trust | ||||||
|       - image: circleci/redis:5-alpine |       - image: circleci/redis:5-alpine | ||||||
|     <<: *test_steps |     <<: *test_steps | ||||||
| 
 | 
 | ||||||
|  | @ -169,9 +171,10 @@ jobs: | ||||||
|     docker: |     docker: | ||||||
|       - image: circleci/ruby:2.6-buster-node |       - image: circleci/ruby:2.6-buster-node | ||||||
|         environment: *ruby_environment |         environment: *ruby_environment | ||||||
|       - image: circleci/postgres:10.6-alpine |       - image: circleci/postgres:12.2 | ||||||
|         environment: |         environment: | ||||||
|           POSTGRES_USER: root |           POSTGRES_USER: root | ||||||
|  |           POSTGRES_HOST_AUTH_METHOD: trust | ||||||
|       - image: circleci/redis:5-alpine |       - image: circleci/redis:5-alpine | ||||||
|     <<: *test_steps |     <<: *test_steps | ||||||
| 
 | 
 | ||||||
|  | @ -180,9 +183,10 @@ jobs: | ||||||
|     docker: |     docker: | ||||||
|       - image: circleci/ruby:2.5-buster-node |       - image: circleci/ruby:2.5-buster-node | ||||||
|         environment: *ruby_environment |         environment: *ruby_environment | ||||||
|       - image: circleci/postgres:10.6-alpine |       - image: circleci/postgres:12.2 | ||||||
|         environment: |         environment: | ||||||
|           POSTGRES_USER: root |           POSTGRES_USER: root | ||||||
|  |           POSTGRES_HOST_AUTH_METHOD: trust | ||||||
|       - image: circleci/redis:5-alpine |       - image: circleci/redis:5-alpine | ||||||
|     <<: *test_steps |     <<: *test_steps | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								Vagrantfile
									
									
									
									
										vendored
									
									
								
							|  | @ -91,7 +91,7 @@ VAGRANTFILE_API_VERSION = "2" | ||||||
| 
 | 
 | ||||||
| Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| | ||||||
| 
 | 
 | ||||||
|   config.vm.box = "ubuntu/xenial64" |   config.vm.box = "ubuntu/bionic64" | ||||||
| 
 | 
 | ||||||
|   config.vm.provider :virtualbox do |vb| |   config.vm.provider :virtualbox do |vb| | ||||||
|     vb.name = "mastodon" |     vb.name = "mastodon" | ||||||
|  |  | ||||||
|  | @ -2,8 +2,18 @@ | ||||||
| 
 | 
 | ||||||
| module Admin | module Admin | ||||||
|   class ActionLogsController < BaseController |   class ActionLogsController < BaseController | ||||||
|     def index |     before_action :set_action_logs | ||||||
|       @action_logs = Admin::ActionLog.page(params[:page]) | 
 | ||||||
|  |     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 |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -9,79 +9,8 @@ module Admin::ActionLogsHelper | ||||||
|     end |     end | ||||||
|   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 |   private | ||||||
| 
 | 
 | ||||||
|   def opposite_verbs?(log) |  | ||||||
|     %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type) |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def linkable_log_target(record) |   def linkable_log_target(record) | ||||||
|     case record.class.name |     case record.class.name | ||||||
|     when 'Account' |     when 'Account' | ||||||
|  | @ -99,7 +28,7 @@ module Admin::ActionLogsHelper | ||||||
|     when 'AccountWarning' |     when 'AccountWarning' | ||||||
|       link_to record.target_account.acct, admin_account_path(record.target_account_id) |       link_to record.target_account.acct, admin_account_path(record.target_account_id) | ||||||
|     when 'Announcement' |     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 | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  | @ -118,7 +47,7 @@ module Admin::ActionLogsHelper | ||||||
|         I18n.t('admin.action_logs.deleted_status') |         I18n.t('admin.action_logs.deleted_status') | ||||||
|       end |       end | ||||||
|     when 'Announcement' |     when 'Announcement' | ||||||
|       "##{attributes['id']}" |       truncate(attributes['text']) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ module Admin::FilterHelper | ||||||
|     InviteFilter::KEYS, |     InviteFilter::KEYS, | ||||||
|     RelationshipFilter::KEYS, |     RelationshipFilter::KEYS, | ||||||
|     AnnouncementFilter::KEYS, |     AnnouncementFilter::KEYS, | ||||||
|  |     Admin::ActionLogFilter::KEYS, | ||||||
|   ].flatten.freeze |   ].flatten.freeze | ||||||
| 
 | 
 | ||||||
|   def filter_link_to(text, link_to_params, link_class_params = link_to_params) |   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 onDomainBlockSeverityChange = (target) => { | ||||||
|   const rejectMediaDiv   = document.querySelector('.input.with_label.domain_block_reject_media'); |   const rejectMediaDiv   = document.querySelector('.input.with_label.domain_block_reject_media'); | ||||||
|   const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports'); |   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 AccountAuthorizeContainer from './containers/account_authorize_container'; | ||||||
| import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; | import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts'; | ||||||
| import ScrollableList from '../../components/scrollable_list'; | import ScrollableList from '../../components/scrollable_list'; | ||||||
|  | import { me } from '../../initial_state'; | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, |   heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, | ||||||
|  | @ -19,6 +20,8 @@ const messages = defineMessages({ | ||||||
| const mapStateToProps = state => ({ | const mapStateToProps = state => ({ | ||||||
|   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), |   accountIds: state.getIn(['user_lists', 'follow_requests', 'items']), | ||||||
|   hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']), |   hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']), | ||||||
|  |   locked: !!state.getIn(['accounts', me, 'locked']), | ||||||
|  |   domain: state.getIn(['meta', 'domain']), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export default @connect(mapStateToProps) | export default @connect(mapStateToProps) | ||||||
|  | @ -31,6 +34,8 @@ class FollowRequests extends ImmutablePureComponent { | ||||||
|     shouldUpdateScroll: PropTypes.func, |     shouldUpdateScroll: PropTypes.func, | ||||||
|     hasMore: PropTypes.bool, |     hasMore: PropTypes.bool, | ||||||
|     accountIds: ImmutablePropTypes.list, |     accountIds: ImmutablePropTypes.list, | ||||||
|  |     locked: PropTypes.bool, | ||||||
|  |     domain: PropTypes.string, | ||||||
|     intl: PropTypes.object.isRequired, |     intl: PropTypes.object.isRequired, | ||||||
|     multiColumn: PropTypes.bool, |     multiColumn: PropTypes.bool, | ||||||
|   }; |   }; | ||||||
|  | @ -44,7 +49,7 @@ class FollowRequests extends ImmutablePureComponent { | ||||||
|   }, 300, { leading: true }); |   }, 300, { leading: true }); | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn } = this.props; |     const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain } = this.props; | ||||||
| 
 | 
 | ||||||
|     if (!accountIds) { |     if (!accountIds) { | ||||||
|       return ( |       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 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 ( |     return ( | ||||||
|       <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}> |       <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}> | ||||||
|  | @ -66,6 +80,7 @@ class FollowRequests extends ImmutablePureComponent { | ||||||
|           shouldUpdateScroll={shouldUpdateScroll} |           shouldUpdateScroll={shouldUpdateScroll} | ||||||
|           emptyMessage={emptyMessage} |           emptyMessage={emptyMessage} | ||||||
|           bindToDocument={!multiColumn} |           bindToDocument={!multiColumn} | ||||||
|  |           prepend={unlockedPrependMessage} | ||||||
|         > |         > | ||||||
|           {accountIds.map(id => |           {accountIds.map(id => | ||||||
|             <AccountAuthorizeContainer key={id} id={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.", |         "defaultMessage": "You don't have any follow requests yet. When you receive one, it will show up here.", | ||||||
|         "id": "empty_column.follow_requests" |         "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" |     "path": "app/javascript/mastodon/features/follow_requests/index.json" | ||||||
|  | @ -2961,4 +2965,4 @@ | ||||||
|     ], |     ], | ||||||
|     "path": "app/javascript/mastodon/features/video/index.json" |     "path": "app/javascript/mastodon/features/video/index.json" | ||||||
|   } |   } | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | @ -168,6 +168,7 @@ | ||||||
|   "errors.unexpected_crash.report_issue": "Report issue", |   "errors.unexpected_crash.report_issue": "Report issue", | ||||||
|   "follow_request.authorize": "Authorize", |   "follow_request.authorize": "Authorize", | ||||||
|   "follow_request.reject": "Reject", |   "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.developers": "Developers", | ||||||
|   "getting_started.directory": "Profile directory", |   "getting_started.directory": "Profile directory", | ||||||
|   "getting_started.documentation": "Documentation", |   "getting_started.documentation": "Documentation", | ||||||
|  |  | ||||||
|  | @ -418,6 +418,11 @@ body, | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     &--with-select strong { | ||||||
|  |       display: block; | ||||||
|  |       margin-bottom: 10px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     a { |     a { | ||||||
|       display: inline-block; |       display: inline-block; | ||||||
|       color: $darker-text-color; |       color: $darker-text-color; | ||||||
|  | @ -583,19 +588,22 @@ body, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .log-entry { | .log-entry { | ||||||
|   margin-bottom: 20px; |  | ||||||
|   line-height: 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 { |   &__header { | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: flex-start; |     justify-content: flex-start; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     padding: 10px; |  | ||||||
|     background: $ui-base-color; |  | ||||||
|     color: $darker-text-color; |     color: $darker-text-color; | ||||||
|     border-radius: 4px 4px 0 0; |  | ||||||
|     font-size: 14px; |     font-size: 14px; | ||||||
|     position: relative; |     padding: 0 10px; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__avatar { |   &__avatar { | ||||||
|  | @ -622,44 +630,6 @@ body, | ||||||
|     color: $dark-text-color; |     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, |   a, | ||||||
|   .username, |   .username, | ||||||
|   .target { |   .target { | ||||||
|  | @ -667,18 +637,6 @@ body, | ||||||
|     text-decoration: none; |     text-decoration: none; | ||||||
|     font-weight: 500; |     font-weight: 500; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   .diff-old { |  | ||||||
|     color: lighten($error-red, 12%); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .diff-neutral { |  | ||||||
|     color: $secondary-text-color; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .diff-new { |  | ||||||
|     color: $success-green; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| a.name-tag, | a.name-tag, | ||||||
|  |  | ||||||
|  | @ -3800,7 +3800,8 @@ a.status-card.compact:hover { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .empty-column-indicator, | .empty-column-indicator, | ||||||
| .error-column { | .error-column, | ||||||
|  | .follow_requests-unlocked_explanation { | ||||||
|   color: $dark-text-color; |   color: $dark-text-color; | ||||||
|   background: $ui-base-color; |   background: $ui-base-color; | ||||||
|   text-align: center; |   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 { | .error-column { | ||||||
|   flex-direction: 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__num= number_with_delimiter @account.targeted_reports.count | ||||||
|       .dashboard__counters__label= t '.targeted_reports' |       .dashboard__counters__label= t '.targeted_reports' | ||||||
|   %div |   %div | ||||||
|     %div |     = link_to admin_action_logs_path(target_account_id: @account.id) do | ||||||
|       .dashboard__counters__text |       .dashboard__counters__text | ||||||
|         - if @account.local? && @account.user.nil? |         - if @account.local? && @account.user.nil? | ||||||
|           %span.neutral= t('admin.accounts.deleted') |           %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 |         = 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 |       .log-entry__timestamp | ||||||
|         %time.formatted{ datetime: action_log.created_at.iso8601 } |         %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 | - content_for :page_title do | ||||||
|   = t('admin.action_logs.title') |   = 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 | = paginate @action_logs | ||||||
|  |  | ||||||
|  | @ -2,6 +2,6 @@ | ||||||
| 
 | 
 | ||||||
| Kaminari.configure do |config| | Kaminari.configure do |config| | ||||||
|   config.default_per_page = 40 |   config.default_per_page = 40 | ||||||
|   config.window = 1 |   config.window = 2 | ||||||
|   config.outer_window = 1 |   config.outer_window = 1 | ||||||
| end | end | ||||||
|  |  | ||||||
|  | @ -195,6 +195,42 @@ en: | ||||||
|       web: Web |       web: Web | ||||||
|       whitelisted: Whitelisted |       whitelisted: Whitelisted | ||||||
|     action_logs: |     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: |       actions: | ||||||
|         assigned_to_self_report: "%{name} assigned report %{target} to themselves" |         assigned_to_self_report: "%{name} assigned report %{target} to themselves" | ||||||
|         change_email_user: "%{name} changed the e-mail address of user %{target}" |         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_custom_emoji: "%{name} updated emoji %{target}" | ||||||
|         update_status: "%{name} updated status by %{target}" |         update_status: "%{name} updated status by %{target}" | ||||||
|       deleted_status: "(deleted status)" |       deleted_status: "(deleted status)" | ||||||
|  |       empty: No logs found. | ||||||
|  |       filter_by_action: Filter by action | ||||||
|  |       filter_by_user: Filter by user | ||||||
|       title: Audit log |       title: Audit log | ||||||
|     announcements: |     announcements: | ||||||
|       destroyed_msg: Announcement successfully deleted! |       destroyed_msg: Announcement successfully deleted! | ||||||
|  |  | ||||||
|  | @ -31,242 +31,4 @@ RSpec.describe Admin::ActionLogsHelper, type: :helper do | ||||||
|       end |       end | ||||||
|     end |     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 | end | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue