Merge branch 'master' into glitch-soc/merge-upstream
commit
d5b5225614
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 New Issue