mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-12 14:16:12 +01:00
Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master
This commit is contained in:
commit
33c2bbdda7
49
app/controllers/admin/change_emails_controller.rb
Normal file
49
app/controllers/admin/change_emails_controller.rb
Normal file
@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ChangeEmailsController < BaseController
|
||||
before_action :set_account
|
||||
before_action :require_local_account!
|
||||
|
||||
def show
|
||||
authorize @user, :change_email?
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @user, :change_email?
|
||||
|
||||
new_email = resource_params.fetch(:unconfirmed_email)
|
||||
|
||||
if new_email != @user.email
|
||||
@user.update!(
|
||||
unconfirmed_email: new_email,
|
||||
# Regenerate the confirmation token:
|
||||
confirmation_token: nil
|
||||
)
|
||||
|
||||
log_action :change_email, @user
|
||||
|
||||
@user.send_confirmation_instructions
|
||||
end
|
||||
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.change_email.changed_msg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
@user = @account.user
|
||||
end
|
||||
|
||||
def require_local_account!
|
||||
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:user).permit(
|
||||
:unconfirmed_email
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
@ -8,19 +8,26 @@ module Admin
|
||||
authorize ReportNote, :create?
|
||||
|
||||
@report_note = current_account.report_notes.new(resource_params)
|
||||
@report = @report_note.report
|
||||
|
||||
if @report_note.save
|
||||
if params[:create_and_resolve]
|
||||
@report_note.report.update!(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
log_action :resolve, @report_note.report
|
||||
@report.resolve!(current_account)
|
||||
log_action :resolve, @report
|
||||
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
|
||||
else
|
||||
redirect_to admin_report_path(@report_note.report_id), notice: I18n.t('admin.report_notes.created_msg')
|
||||
return
|
||||
end
|
||||
|
||||
if params[:create_and_unresolve]
|
||||
@report.unresolve!
|
||||
log_action :reopen, @report
|
||||
end
|
||||
|
||||
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
|
||||
else
|
||||
@report = @report_note.report
|
||||
@report_notes = @report.notes.latest
|
||||
@report_history = @report.history
|
||||
@form = Form::StatusBatch.new
|
||||
|
||||
render template: 'admin/reports/show'
|
||||
|
@ -13,6 +13,7 @@ module Admin
|
||||
authorize @report, :show?
|
||||
@report_note = @report.notes.new
|
||||
@report_notes = @report.notes.latest
|
||||
@report_history = @report.history
|
||||
@form = Form::StatusBatch.new
|
||||
end
|
||||
|
||||
@ -38,36 +39,33 @@ module Admin
|
||||
@report.update!(assigned_account_id: nil)
|
||||
log_action :unassigned, @report
|
||||
when 'reopen'
|
||||
@report.update!(action_taken: false, action_taken_by_account_id: nil)
|
||||
@report.unresolve!
|
||||
log_action :reopen, @report
|
||||
when 'resolve'
|
||||
@report.update!(action_taken_by_current_attributes)
|
||||
@report.resolve!(current_account)
|
||||
log_action :resolve, @report
|
||||
when 'suspend'
|
||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||
|
||||
log_action :resolve, @report
|
||||
log_action :suspend, @report.target_account
|
||||
|
||||
resolve_all_target_account_reports
|
||||
@report.reload
|
||||
when 'silence'
|
||||
@report.target_account.update!(silenced: true)
|
||||
|
||||
log_action :resolve, @report
|
||||
log_action :silence, @report.target_account
|
||||
|
||||
resolve_all_target_account_reports
|
||||
@report.reload
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
def action_taken_by_current_attributes
|
||||
{ action_taken: true, action_taken_by_account_id: current_account.id }
|
||||
@report.reload
|
||||
end
|
||||
|
||||
def resolve_all_target_account_reports
|
||||
unresolved_reports_for_target_account.update_all(
|
||||
action_taken_by_current_attributes
|
||||
)
|
||||
unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
|
||||
end
|
||||
|
||||
def unresolved_reports_for_target_account
|
||||
|
@ -45,6 +45,8 @@ module Admin::ActionLogsHelper
|
||||
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
|
||||
@ -84,7 +86,7 @@ module Admin::ActionLogsHelper
|
||||
'positive'
|
||||
when :create
|
||||
opposite_verbs?(log) ? 'negative' : 'positive'
|
||||
when :update, :reset_password, :disable_2fa, :memorialize
|
||||
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
|
||||
'neutral'
|
||||
when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
|
||||
'negative'
|
||||
|
33
app/javascript/mastodon/components/load_gap.js
Normal file
33
app/javascript/mastodon/components/load_gap.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
export default class LoadGap extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
maxId: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick(this.props.maxId);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { disabled, intl } = this.props;
|
||||
|
||||
return (
|
||||
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
|
||||
<i className='fa fa-ellipsis-h' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -31,6 +31,8 @@ export default class Status extends ImmutablePureComponent {
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onPin: PropTypes.func,
|
||||
onOpenMedia: PropTypes.func,
|
||||
onOpenVideo: PropTypes.func,
|
||||
|
@ -9,6 +9,7 @@ import { me } from '../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
@ -41,6 +42,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
@ -92,6 +94,10 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
|
||||
handleMuteClick = () => {
|
||||
this.props.onMute(this.props.status.get('account'));
|
||||
}
|
||||
@ -149,6 +155,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
|
@ -4,28 +4,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import StatusContainer from '../containers/status_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import LoadMore from './load_more';
|
||||
import LoadGap from './load_gap';
|
||||
import ScrollableList from './scrollable_list';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
class LoadGap extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
maxId: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick(this.props.maxId);
|
||||
}
|
||||
|
||||
render () {
|
||||
return <LoadMore onClick={this.handleClick} disabled={this.props.disabled} />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class StatusList extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -5,6 +5,7 @@ import { makeGetStatus } from '../selectors';
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from '../actions/compose';
|
||||
import {
|
||||
reblog,
|
||||
@ -102,6 +103,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
},
|
||||
|
||||
onMention (account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
@ -13,7 +13,7 @@ import { createSelector } from 'reselect';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import LoadGap from '../../components/load_gap';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
@ -24,24 +24,6 @@ const getNotifications = createSelector([
|
||||
state => state.getIn(['notifications', 'items']),
|
||||
], (excludedTypes, notifications) => notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))));
|
||||
|
||||
class LoadGap extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
maxId: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick(this.props.maxId);
|
||||
}
|
||||
|
||||
render () {
|
||||
return <LoadMore onClick={this.handleClick} disabled={this.props.disabled} />;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
notifications: getNotifications(state),
|
||||
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
||||
|
@ -8,6 +8,7 @@ import { me } from '../../../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
|
||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
@ -37,6 +38,7 @@ export default class ActionBar extends React.PureComponent {
|
||||
onReblog: PropTypes.func.isRequired,
|
||||
onFavourite: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func,
|
||||
onMuteConversation: PropTypes.func,
|
||||
@ -63,6 +65,10 @@ export default class ActionBar extends React.PureComponent {
|
||||
this.props.onDelete(this.props.status);
|
||||
}
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
|
||||
handleMentionClick = () => {
|
||||
this.props.onMention(this.props.status.get('account'), this.context.router.history);
|
||||
}
|
||||
@ -108,6 +114,7 @@ export default class ActionBar extends React.PureComponent {
|
||||
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (me === status.getIn(['account', 'id'])) {
|
||||
@ -121,6 +128,7 @@ export default class ActionBar extends React.PureComponent {
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
directCompose,
|
||||
} from '../../actions/compose';
|
||||
import { blockAccount } from '../../actions/accounts';
|
||||
import {
|
||||
@ -148,6 +149,10 @@ export default class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleDirectClick = (account, router) => {
|
||||
this.props.dispatch(directCompose(account, router));
|
||||
}
|
||||
|
||||
handleMentionClick = (account, router) => {
|
||||
this.props.dispatch(mentionCompose(account, router));
|
||||
}
|
||||
@ -379,6 +384,7 @@ export default class Status extends ImmutablePureComponent {
|
||||
onFavourite={this.handleFavouriteClick}
|
||||
onReblog={this.handleReblogClick}
|
||||
onDelete={this.handleDeleteClick}
|
||||
onDirect={this.handleDirectClick}
|
||||
onMention={this.handleMentionClick}
|
||||
onMute={this.handleMuteClick}
|
||||
onMuteConversation={this.handleConversationMuteClick}
|
||||
|
@ -197,6 +197,10 @@
|
||||
"defaultMessage": "Delete",
|
||||
"id": "status.delete"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Direct message @{name}",
|
||||
"id": "status.direct"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Mention @{name}",
|
||||
"id": "status.mention"
|
||||
@ -1370,6 +1374,10 @@
|
||||
"defaultMessage": "Delete",
|
||||
"id": "status.delete"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Direct message @{name}",
|
||||
"id": "status.direct"
|
||||
},
|
||||
{
|
||||
"defaultMessage": "Mention @{name}",
|
||||
"id": "status.mention"
|
||||
|
@ -247,6 +247,7 @@
|
||||
"status.block": "Block @{name}",
|
||||
"status.cannot_reblog": "This post cannot be boosted",
|
||||
"status.delete": "Delete",
|
||||
"status.direct": "Direct message @{name}",
|
||||
"status.embed": "Embed",
|
||||
"status.favourite": "Favourite",
|
||||
"status.load_more": "Load more",
|
||||
|
@ -276,6 +276,7 @@
|
||||
"tabs_bar.home": "Strona główna",
|
||||
"tabs_bar.local_timeline": "Lokalne",
|
||||
"tabs_bar.notifications": "Powiadomienia",
|
||||
"tabs_bar.search": "Szukaj",
|
||||
"ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Mastodona.",
|
||||
"upload_area.title": "Przeciągnij i upuść aby wysłać",
|
||||
"upload_button.label": "Dodaj zawartość multimedialną",
|
||||
|
@ -259,16 +259,18 @@ export default function compose(state = initialState, action) {
|
||||
case COMPOSE_UPLOAD_PROGRESS:
|
||||
return state.set('progress', Math.round((action.loaded / action.total) * 100));
|
||||
case COMPOSE_MENTION:
|
||||
return state
|
||||
.update('text', text => `${text}@${action.account.get('acct')} `)
|
||||
.set('focusDate', new Date())
|
||||
.set('idempotencyKey', uuid());
|
||||
return state.withMutations(map => {
|
||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
||||
map.set('focusDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_DIRECT:
|
||||
return state
|
||||
.update('text', text => `@${action.account.get('acct')} `)
|
||||
.set('privacy', 'direct')
|
||||
.set('focusDate', new Date())
|
||||
.set('idempotencyKey', uuid());
|
||||
return state.withMutations(map => {
|
||||
map.update('text', text => [text.trim(), `@${action.account.get('acct')} `].filter((str) => str.length !== 0).join(' '));
|
||||
map.set('privacy', 'direct');
|
||||
map.set('focusDate', new Date());
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SUGGESTIONS_CLEAR:
|
||||
return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
|
||||
case COMPOSE_SUGGESTIONS_READY:
|
||||
|
@ -145,6 +145,11 @@
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid $ui-base-color;
|
||||
|
||||
&.section-break {
|
||||
margin: 30px 0;
|
||||
border-bottom: 2px solid $ui-base-lighter-color;
|
||||
}
|
||||
}
|
||||
|
||||
.muted-hint {
|
||||
@ -330,6 +335,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.report-note__comment {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.report-note__form {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.report-note__textarea {
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
padding: 7px 4px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
color: $ui-base-color;
|
||||
display: block;
|
||||
width: 100%;
|
||||
outline: 0;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.report-note__buttons {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.report-note__button {
|
||||
margin: 0 0 5px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.batch-form-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -2455,6 +2455,10 @@ a.status-card {
|
||||
}
|
||||
}
|
||||
|
||||
.load-gap {
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.regeneration-indicator {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
|
@ -126,6 +126,7 @@ class Account < ApplicationRecord
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
|
||||
delegate :email,
|
||||
:unconfirmed_email,
|
||||
:current_sign_in_ip,
|
||||
:current_sign_in_at,
|
||||
:confirmed?,
|
||||
|
@ -35,6 +35,11 @@ class Admin::ActionLog < ApplicationRecord
|
||||
self.recorded_changes = target.attributes
|
||||
when :update, :promote, :demote
|
||||
self.recorded_changes = target.previous_changes
|
||||
when :change_email
|
||||
self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new(
|
||||
email: [target.email, nil],
|
||||
unconfirmed_email: [nil, target.unconfirmed_email]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -15,16 +15,12 @@ module StatusThreadingConcern
|
||||
|
||||
def ancestor_ids
|
||||
Rails.cache.fetch("ancestors:#{id}") do
|
||||
ancestors_without_self.pluck(:id)
|
||||
ancestor_statuses.pluck(:id)
|
||||
end
|
||||
end
|
||||
|
||||
def ancestors_without_self
|
||||
ancestor_statuses - [self]
|
||||
end
|
||||
|
||||
def ancestor_statuses
|
||||
Status.find_by_sql([<<-SQL.squish, id: id])
|
||||
Status.find_by_sql([<<-SQL.squish, id: in_reply_to_id])
|
||||
WITH RECURSIVE search_tree(id, in_reply_to_id, path)
|
||||
AS (
|
||||
SELECT id, in_reply_to_id, ARRAY[id]
|
||||
@ -43,11 +39,7 @@ module StatusThreadingConcern
|
||||
end
|
||||
|
||||
def descendant_ids
|
||||
descendants_without_self.pluck(:id)
|
||||
end
|
||||
|
||||
def descendants_without_self
|
||||
descendant_statuses - [self]
|
||||
descendant_statuses.pluck(:id)
|
||||
end
|
||||
|
||||
def descendant_statuses
|
||||
@ -56,7 +48,7 @@ module StatusThreadingConcern
|
||||
AS (
|
||||
SELECT id, ARRAY[id]
|
||||
FROM statuses
|
||||
WHERE id = :id
|
||||
WHERE in_reply_to_id = :id
|
||||
UNION ALL
|
||||
SELECT statuses.id, path || statuses.id
|
||||
FROM search_tree
|
||||
|
@ -58,5 +58,9 @@ class CustomEmoji < ApplicationRecord
|
||||
|
||||
where(shortcode: shortcodes, domain: domain, disabled: false)
|
||||
end
|
||||
|
||||
def search(shortcode)
|
||||
where('"custom_emojis"."shortcode" ILIKE ?', "%#{shortcode}%")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class CustomEmojiFilter
|
||||
when 'by_domain'
|
||||
CustomEmoji.where(domain: value)
|
||||
when 'shortcode'
|
||||
CustomEmoji.where(shortcode: value)
|
||||
CustomEmoji.search(value)
|
||||
else
|
||||
raise "Unknown filter: #{key}"
|
||||
end
|
||||
|
@ -150,8 +150,9 @@ class MediaAttachment < ApplicationRecord
|
||||
'pix_fmt' => 'yuv420p',
|
||||
'vf' => 'scale=\'trunc(iw/2)*2:trunc(ih/2)*2\'',
|
||||
'vsync' => 'cfr',
|
||||
'b:v' => '1300K',
|
||||
'maxrate' => '500K',
|
||||
'c:v' => 'h264',
|
||||
'b:v' => '500K',
|
||||
'maxrate' => '1300K',
|
||||
'bufsize' => '1300K',
|
||||
'crf' => 18,
|
||||
},
|
||||
|
@ -39,4 +39,50 @@ class Report < ApplicationRecord
|
||||
def media_attachments
|
||||
MediaAttachment.where(status_id: status_ids)
|
||||
end
|
||||
|
||||
def assign_to_self!(current_account)
|
||||
update!(assigned_account_id: current_account.id)
|
||||
end
|
||||
|
||||
def unassign!
|
||||
update!(assigned_account_id: nil)
|
||||
end
|
||||
|
||||
def resolve!(acting_account)
|
||||
update!(action_taken: true, action_taken_by_account_id: acting_account.id)
|
||||
end
|
||||
|
||||
def unresolve!
|
||||
update!(action_taken: false, action_taken_by_account_id: nil)
|
||||
end
|
||||
|
||||
def unresolved?
|
||||
!action_taken?
|
||||
end
|
||||
|
||||
def history
|
||||
time_range = created_at..updated_at
|
||||
|
||||
sql = [
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Report',
|
||||
target_id: id,
|
||||
created_at: time_range
|
||||
).unscope(:order),
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Account',
|
||||
target_id: target_account_id,
|
||||
created_at: time_range
|
||||
).unscope(:order),
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Status',
|
||||
target_id: status_ids,
|
||||
created_at: time_range
|
||||
).unscope(:order),
|
||||
].map { |query| "(#{query.to_sql})" }.join(' UNION ALL ')
|
||||
|
||||
Admin::ActionLog.from("(#{sql}) AS admin_action_logs")
|
||||
end
|
||||
end
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
class ReportNote < ApplicationRecord
|
||||
belongs_to :account
|
||||
belongs_to :report, inverse_of: :notes
|
||||
belongs_to :report, inverse_of: :notes, touch: true
|
||||
|
||||
scope :latest, -> { reorder('created_at ASC') }
|
||||
|
||||
|
@ -5,6 +5,10 @@ class UserPolicy < ApplicationPolicy
|
||||
staff? && !record.staff?
|
||||
end
|
||||
|
||||
def change_email?
|
||||
staff? && !record.staff?
|
||||
end
|
||||
|
||||
def disable_2fa?
|
||||
admin? && !record.staff?
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class PostStatusService < BaseService
|
||||
status = account.statuses.create!(text: text,
|
||||
media_attachments: media || [],
|
||||
thread: in_reply_to,
|
||||
sensitive: options[:sensitive],
|
||||
sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]),
|
||||
spoiler_text: options[:spoiler_text] || '',
|
||||
visibility: options[:visibility] || account.user&.setting_default_privacy,
|
||||
language: LanguageDetector.instance.detect(text, account),
|
||||
|
@ -36,9 +36,13 @@
|
||||
%th= t('admin.accounts.email')
|
||||
%td
|
||||
= @account.user_email
|
||||
|
||||
- if @account.user_confirmed?
|
||||
= fa_icon('check')
|
||||
= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
|
||||
- if @account.user_unconfirmed_email.present?
|
||||
%th= t('admin.accounts.unconfirmed_email')
|
||||
%td
|
||||
= @account.user_unconfirmed_email
|
||||
%tr
|
||||
%th= t('admin.accounts.login_status')
|
||||
%td
|
||||
|
7
app/views/admin/change_emails/show.html.haml
Normal file
7
app/views/admin/change_emails/show.html.haml
Normal file
@ -0,0 +1,7 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.accounts.change_email.title', username: @account.acct)
|
||||
|
||||
= simple_form_for @user, url: admin_account_change_email_path(@account.id) do |f|
|
||||
= f.input :email, wrapper: :with_label, disabled: true, label: t('admin.accounts.change_email.current_email')
|
||||
= f.input :unconfirmed_email, wrapper: :with_label, label: t('admin.accounts.change_email.new_email')
|
||||
= f.button :submit, class: "button", value: t('admin.accounts.change_email.submit')
|
@ -1,11 +1,9 @@
|
||||
%tr
|
||||
%td
|
||||
%p
|
||||
%strong= report_note.account.acct
|
||||
on
|
||||
%li
|
||||
%h4
|
||||
= report_note.account.acct
|
||||
%div{ style: 'float: right' }
|
||||
%time.formatted{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
|
||||
= l report_note.created_at
|
||||
= table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
|
||||
%br/
|
||||
%br/
|
||||
%div{ class: 'report-note__comment' }
|
||||
= simple_format(h(report_note.content))
|
||||
|
@ -2,7 +2,7 @@
|
||||
= t('admin.reports.report', id: @report.id)
|
||||
|
||||
%div{ style: 'overflow: hidden; margin-bottom: 20px' }
|
||||
- if !@report.action_taken?
|
||||
- if @report.unresolved?
|
||||
%div{ style: 'float: right' }
|
||||
= link_to t('admin.reports.silence_account'), admin_report_path(@report, outcome: 'silence'), method: :put, class: 'button'
|
||||
= link_to t('admin.reports.suspend_account'), admin_report_path(@report, outcome: 'suspend'), method: :put, class: 'button'
|
||||
@ -14,22 +14,29 @@
|
||||
.table-wrapper
|
||||
%table.table.inline-table
|
||||
%tbody
|
||||
%tr
|
||||
%th= t('admin.reports.created_at')
|
||||
%td{colspan: 2}
|
||||
%time.formatted{ datetime: @report.created_at.iso8601 }
|
||||
%tr
|
||||
%th= t('admin.reports.updated_at')
|
||||
%td{colspan: 2}
|
||||
%time.formatted{ datetime: @report.updated_at.iso8601 }
|
||||
%tr
|
||||
%th= t('admin.reports.status')
|
||||
%td{colspan: 2}
|
||||
%td
|
||||
- if @report.action_taken?
|
||||
= t('admin.reports.resolved')
|
||||
= table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
|
||||
- else
|
||||
= t('admin.reports.unresolved')
|
||||
%td{style: "text-align: right; overflow: hidden;"}
|
||||
- if @report.action_taken?
|
||||
= table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
|
||||
- if !@report.action_taken_by_account.nil?
|
||||
%tr
|
||||
%th= t('admin.reports.action_taken_by')
|
||||
%td= @report.action_taken_by_account.acct
|
||||
%td{colspan: 2}
|
||||
= @report.action_taken_by_account.acct
|
||||
- else
|
||||
%tr
|
||||
%th= t('admin.reports.assigned')
|
||||
@ -44,6 +51,8 @@
|
||||
- if !@report.assigned_account.nil?
|
||||
= table_link_to 'trash', t('admin.reports.unassign'), admin_report_path(@report, outcome: 'unassign'), method: :put
|
||||
|
||||
%hr{ class: "section-break"}/
|
||||
|
||||
.report-accounts
|
||||
.report-accounts__item
|
||||
%h3= t('admin.reports.reported_account')
|
||||
@ -85,22 +94,28 @@
|
||||
= link_to admin_report_reported_status_path(@report, status), method: :delete, class: 'icon-button trash-button', title: t('admin.reports.delete'), data: { confirm: t('admin.reports.are_you_sure') }, remote: true do
|
||||
= fa_icon 'trash'
|
||||
|
||||
%hr/
|
||||
%hr{ class: "section-break"}/
|
||||
|
||||
%h3= t('admin.reports.notes.label')
|
||||
|
||||
- if @report_notes.length > 0
|
||||
.table-wrapper
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th
|
||||
%tbody
|
||||
%ul
|
||||
= render @report_notes
|
||||
|
||||
= simple_form_for @report_note, url: admin_report_notes_path do |f|
|
||||
%h4= t('admin.reports.notes.new_label')
|
||||
= form_for @report_note, url: admin_report_notes_path, html: { class: 'report-note__form' } do |f|
|
||||
= render 'shared/error_messages', object: @report_note
|
||||
= f.input :content
|
||||
= f.text_area :content, placeholder: t('admin.reports.notes.placeholder'), rows: 6, class: 'report-note__textarea'
|
||||
= f.hidden_field :report_id
|
||||
= f.button :button, t('admin.reports.notes.create'), type: :submit
|
||||
= f.button :button, t('admin.reports.notes.create_and_resolve'), type: :submit, name: :create_and_resolve
|
||||
%div{ class: 'report-note__buttons' }
|
||||
- if @report.unresolved?
|
||||
= f.submit t('admin.reports.notes.create_and_resolve'), name: :create_and_resolve, class: 'button report-note__button'
|
||||
- else
|
||||
= f.submit t('admin.reports.notes.create_and_unresolve'), name: :create_and_unresolve, class: 'button report-note__button'
|
||||
= f.submit t('admin.reports.notes.create'), class: 'button report-note__button'
|
||||
|
||||
- if @report_history.length > 0
|
||||
%h3= t('admin.reports.history')
|
||||
|
||||
%ul
|
||||
= render @report_history
|
||||
|
4
config/initializers/rack_attack_logging.rb
Normal file
4
config/initializers/rack_attack_logging.rb
Normal file
@ -0,0 +1,4 @@
|
||||
ActiveSupport::Notifications.subscribe('rack.attack') do |_name, _start, _finish, _request_id, req|
|
||||
next unless [:throttle, :blacklist].include? req.env['rack.attack.match_type']
|
||||
Rails.logger.info("Rate limit hit (#{req.env['rack.attack.match_type']}): #{req.ip} #{req.request_method} #{req.fullpath}")
|
||||
end
|
@ -4,7 +4,7 @@ namespace = ENV.fetch('REDIS_NAMESPACE') { nil }
|
||||
redis_params = { url: ENV['REDIS_URL'] }
|
||||
|
||||
if namespace
|
||||
redis_params [:namespace] = namespace
|
||||
redis_params[:namespace] = namespace
|
||||
end
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
@ -18,3 +18,5 @@ end
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = redis_params
|
||||
end
|
||||
|
||||
Sidekiq::Logging.logger.level = ::Logger::const_get(ENV.fetch('RAILS_LOG_LEVEL', 'info').upcase.to_s)
|
||||
|
@ -63,6 +63,13 @@ en:
|
||||
are_you_sure: Are you sure?
|
||||
avatar: Avatar
|
||||
by_domain: Domain
|
||||
change_email:
|
||||
changed_msg: Account email successfully changed!
|
||||
current_email: Current Email
|
||||
label: Change Email
|
||||
new_email: New Email
|
||||
submit: Change Email
|
||||
title: Change Email for %{username}
|
||||
confirm: Confirm
|
||||
confirmed: Confirmed
|
||||
demote: Demote
|
||||
@ -131,6 +138,7 @@ en:
|
||||
statuses: Statuses
|
||||
subscribe: Subscribe
|
||||
title: Accounts
|
||||
unconfirmed_email: Unconfirmed E-mail
|
||||
undo_silenced: Undo silence
|
||||
undo_suspension: Undo suspension
|
||||
unsubscribe: Unsubscribe
|
||||
@ -139,6 +147,7 @@ en:
|
||||
action_logs:
|
||||
actions:
|
||||
assigned_to_self_report: "%{name} assigned report %{target} to themselves"
|
||||
change_email_user: "%{name} changed the e-mail address of user %{target}"
|
||||
confirm_user: "%{name} confirmed e-mail address of user %{target}"
|
||||
create_custom_emoji: "%{name} uploaded new emoji %{target}"
|
||||
create_domain_block: "%{name} blocked domain %{target}"
|
||||
@ -247,8 +256,8 @@ en:
|
||||
title: Filter
|
||||
title: Invites
|
||||
report_notes:
|
||||
created_msg: Moderation note successfully created!
|
||||
destroyed_msg: Moderation note successfully destroyed!
|
||||
created_msg: Report note successfully created!
|
||||
destroyed_msg: Report note successfully deleted!
|
||||
reports:
|
||||
action_taken_by: Action taken by
|
||||
are_you_sure: Are you sure?
|
||||
@ -257,15 +266,20 @@ en:
|
||||
comment:
|
||||
label: Report Comment
|
||||
none: None
|
||||
created_at: Reported
|
||||
delete: Delete
|
||||
history: Moderation History
|
||||
id: ID
|
||||
mark_as_resolved: Mark as resolved
|
||||
mark_as_unresolved: Mark as unresolved
|
||||
notes:
|
||||
create: Add Note
|
||||
create_and_resolve: Resolve with Note
|
||||
create_and_unresolve: Reopen with Note
|
||||
delete: Delete
|
||||
label: Notes
|
||||
label: Moderator Notes
|
||||
new_label: Add Moderator Note
|
||||
placeholder: Describe what actions have been taken, or any other updates to this report…
|
||||
nsfw:
|
||||
'false': Unhide media attachments
|
||||
'true': Hide media attachments
|
||||
|
@ -711,6 +711,83 @@ pl:
|
||||
reblogged: podbił
|
||||
sensitive_content: Wrażliwa zawartość
|
||||
terms:
|
||||
body_html: |
|
||||
<h2>Polityka prywatności</h2>
|
||||
<h3 id="collect">Jakie informacje zbieramy?</h3>
|
||||
|
||||
<ul>
|
||||
<li><em>Podstawowe informacje o koncie</em>: Podczas rejestracji na tym serwerze, możesz zostać poproszony o wprowadzenie nazwy użytkownika, adresu e-mail i hasła. Możesz także wprowadzić dodatkowe informacje o profilu, takie jak nazwa wyświetlana i biografia oraz wysłać awatar i obraz nagłówka. Nazwa użytkownika, nazwa wyświetlana, biografia, awatar i obraz nagłówka są zawsze widoczne dla wszystkich.</li>
|
||||
<li><em>Wpisy, śledzenie i inne publiczne informacje</em>: Lista osób które śledzisz jest widoczna publicznie, tak jak lista osób, które Cię śledzą. Jeżeli dodasz wpis, data i czas jego utworzenia i aplikacja, z której go wysłano są przechowywane. Wiadomości mogą zawierać załączniki multimedialne, takie jak zdjęcia i filmy. Publiczne i niewidoczne wpisy są dostępne publicznie. Udostępniony wpis również jest widoczny publicznie. Twoje wpisy są dostarczane obserwującym, co oznacza że jego kopie mogą zostać dostarczone i być przechowywane na innych serwerach. Kiedy usuniesz wpis, przestaje być widoczny również dla osób śledzących Cię. „Podbijanie” i dodanie do ulubionych jest zawsze publiczne.</li>
|
||||
<li><em>Wpisy bezpośrednie i tylko dla śledzących</em>: Wszystkie wpisy są przechowywane i przetwarzane na serwerze. Wpisy przeznaczone tylko dla śledzących są widoczne tylko dla nich i osób wspomnianych we wpisie, a wpisy bezpośrednie tylko dla wspimnianych. W wielu przypadkach oznacza to, że ich kopie są dostarczane i przechowywane na innych serwerach. Staramy się ograniczać zasięg tych wpisów wyłącznie do właściwych odbiorców, ale inne serwery mogą tego nie robić. Ważne jest, aby sprawdzać jakich serwerów używają osoby, które Cię śledzą. Możesz aktywować opcję pozwalającą na ręczne akceptowanie i odrzucanie nowych śledzących. <em>Pamiętaj, że właściciele serwerów mogą zobaczyć te wiadomości</em>, a odbiorcy mogą wykonać zrzut ekranu, skopiować lub udostępniać ten wpis. <em>Nie udostępniaj wrażliwych danych z użyciem Mastodona.</em></li>
|
||||
<li><em>Adresy IP i inne metadane</em>: Kiedy zalogujesz się, przechowujemy adres IP użyty w trakcie logowania wraz z nazwą używanej przeglądarki. Wszystkie aktywne sesje możesz zobaczyć (i wygasić) w ustawieniach. Ostatnio używany adres IP jest przechowywany przez nas do 12 miesięcy. Możemy również przechowywać adresy IP wykorzystywane przy każdym działaniu na serwerze.</li>
|
||||
</ul>
|
||||
|
||||
<hr class="spacer" />
|
||||
|
||||
<h3 id="use">W jakim celu wykorzystujecie informacje?</h3>
|
||||
|
||||
<p>Zebrane informacje mogą zostać użyte w następujące sposoby:</p>
|
||||
|
||||
<ul>
|
||||
<li>Aby dostarczyć podstawową funkcjonalność Mastodona. Możesz wchodzić w interakcje z zawartością tworzoną przez innych tylko gdy jesteś zalogowany. Na przykład, możesz śledzić innych, aby widzieć ich wpisy w dostosowanej osi czasu.</li>
|
||||
<li>Aby wspomóc moderację społeczności, na przykład porównując Twój adres IP ze znanymi, aby rozpoznać próbę obejścia blokady i inne naruszenia.</li>
|
||||
<li>Adres e-mail może zostać wykorzystany, aby wysyłać Ci informacje, powiadomienia o osobach wchodzących w interakcje z tworzoną przez Ciebie zawartością, wysyłających Ci wiadomości, odpowiadać na zgłoszenia i inne żądania lub zapytania.</li>
|
||||
</ul>
|
||||
|
||||
<hr class="spacer" />
|
||||
|
||||
<h3 id="protect">W jaki sposób chronimy Twoje dane?</h3>
|
||||
|
||||
<p>Wykorzystujemy różne zabezpieczenia, aby zapewnić bezpieczeństwo informacji, które wprowadzasz, wysyłasz lub do których uzyskujesz dostęp. Poza tym, sesja przeglądarki oraz ruch pomiędzy aplikacją a API jest zabezpieczany z użyciem SSL, a hasło jest hashowane z użyciem silnego algorytmu. Możesz też aktywować uwierzytelnianie dwustopniowe, aby lepiej zabezpieczyć dostęp do konta.</p>
|
||||
|
||||
<hr class="spacer" />
|
||||
|
||||
<h3 id="data-retention">Jaka jest nasza polityka przechowywania danych?</h3>
|
||||
|
||||
<p>Staramy się:</p>
|
||||
|
||||
<ul>
|
||||
<li>Przechowywać logi zawierające adresy IP używane przy każdym żądaniu do serwera przez nie dłużej niż 90 dni.</li>
|
||||
<li>Przechowywać adresy IP przypisane do użytkowników przez nie dłużej niż 12 miesięcy.</li>
|
||||
</ul>
|
||||
|
||||
<p>Możesz zażądać i pobrać archiwum tworzonej zawartości, wliczając Twoje wpisy, załączniki multimedialne, awatar i zdjęcie nagłówka.</p>
|
||||
|
||||
<p>Możesz nieodwracalnie usunąć konto w każdej chwili.</p>
|
||||
|
||||
<hr class="spacer"/>
|
||||
|
||||
<h3 id="cookies">Czy używany plików cookies?</h3>
|
||||
|
||||
<p>Tak. Pliki cookies są małymi plikami, które strona lub dostawca jej usługi dostarcza na dysk twardy komputera z użyciem przeglądarki internetowej (jeżeli na to pozwoli). Pliki cookies pozwalają na rozpoznanie przeglądarki i – jeśli jesteś zarejestrowany – przypisanie jej do konta.</p>
|
||||
|
||||
<p>Wykorzystujemy pliki cookies, aby przechowywać preferencję użytkowników na przyszłe wizyty.</p>
|
||||
|
||||
<hr class="spacer" />
|
||||
|
||||
<h3 id="disclose">Czy przekazujemy informacje osobom trzecim?</h3>
|
||||
|
||||
<p>Nie sprzedajemy, nie wymieniamy i nie przekazujemy osobom trzecim informacji pozwalających na identyfikację Ciebie. Nie dotyczy to zaufanym dostawcom pomagającym w prowadzeniu lub obsługiwaniu użytkowników, jeżeli zgadzają się, aby nie przekazywać dalej tych informacji. Możemy również udostępnić informacje, jeżeli uważany to za wymagane przez prawo, konieczne do wypełnienia polityki strony, przestrzegania naszych lub cudzych praw, własności i bezpieczeństwa.</p>
|
||||
|
||||
<p>Twoja publiczna zawartość może zostać pobrana przez inne serwery w sieci. Wpisy publiczne i tylko dla śledzących są dostarczane na serwery, na których znajdują się śledzący Cię, a wiadomości bezpośrednie trafiają na serwery adresatów, jeżeli są oni użytkownikami innego serwera.</p>
|
||||
|
||||
<p>Kiedy pozwolisz aplikacji na dostęp do Twojego konta, w zależności od nadanych jej pozwoleń, może uzyskać dostęp do publicznych informacji, listy śledzonych, Twoich list, wszystkich wpisów i ulubionych. Aplikacje nie mogą uzyskać dostępu do Twojego adresu e-mail i hasła.</p>
|
||||
|
||||
<hr class="spacer" />
|
||||
|
||||
<h3 id="coppa">Children's Online Privacy Protection Act Compliance</h3>
|
||||
|
||||
<p>Ta strona, produkty i usługi są przeznaczone dla osób, które ukończyły 13 lat. Jeżeli serwer znajduje się w USA, a nie ukończyłeś 13 roku życia, zgodnie z wymogami COPPA (<a href="https://pl.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act">Prawo o Ochronie Prywatności Dzieci w Internecie</a>), nie używaj tej strony.</p>
|
||||
|
||||
<hr class="spacer" />
|
||||
|
||||
<h3 id="changes">Zmiany w naszej polityce prywatności</h3>
|
||||
|
||||
<p>Jeżeli zdecydujemy się na zmiany w polityce prywatności, pojawią się na tej stronie.</p>
|
||||
|
||||
<p>Dokument jest dostępny na licencji CC-BY-SA. Ostatnio zmodyfikowano go 7 marca 2018, przetłumaczono 9 kwietnia 2018. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.</p>
|
||||
|
||||
<p>Bazowano na <a href="https://github.com/discourse/discourse">polityce prywatności Discourse</a>.</p>
|
||||
title: Zasady korzystania i polityka prywatności %{instance}
|
||||
time:
|
||||
formats:
|
||||
|
@ -160,6 +160,7 @@ Rails.application.routes.draw do
|
||||
post :memorialize
|
||||
end
|
||||
|
||||
resource :change_email, only: [:show, :update]
|
||||
resource :reset, only: [:create]
|
||||
resource :silence, only: [:create, :destroy]
|
||||
resource :suspension, only: [:create, :destroy]
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 81 B |
47
spec/controllers/admin/change_email_controller_spec.rb
Normal file
47
spec/controllers/admin/change_email_controller_spec.rb
Normal file
@ -0,0 +1,47 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Admin::ChangeEmailsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:admin) { Fabricate(:user, admin: true) }
|
||||
|
||||
before do
|
||||
sign_in admin
|
||||
end
|
||||
|
||||
describe "GET #show" do
|
||||
it "returns http success" do
|
||||
account = Fabricate(:account)
|
||||
user = Fabricate(:user, account: account)
|
||||
|
||||
get :show, params: { account_id: account.id }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #update" do
|
||||
before do
|
||||
allow(UserMailer).to receive(:confirmation_instructions).and_return(double('email', deliver_later: nil))
|
||||
end
|
||||
|
||||
it "returns http success" do
|
||||
account = Fabricate(:account)
|
||||
user = Fabricate(:user, account: account)
|
||||
|
||||
previous_email = user.email
|
||||
|
||||
post :update, params: { account_id: account.id, user: { unconfirmed_email: 'test@example.com' } }
|
||||
|
||||
user.reload
|
||||
|
||||
expect(user.email).to eq previous_email
|
||||
expect(user.unconfirmed_email).to eq 'test@example.com'
|
||||
expect(user.confirmation_token).not_to be_nil
|
||||
|
||||
expect(UserMailer).to have_received(:confirmation_instructions).with(user, user.confirmation_token, { to: 'test@example.com' })
|
||||
|
||||
expect(response).to redirect_to(admin_account_path(account.id))
|
||||
end
|
||||
end
|
||||
end
|
@ -1,6 +1,30 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CustomEmoji, type: :model do
|
||||
describe '#search' do
|
||||
let(:custom_emoji) { Fabricate(:custom_emoji, shortcode: shortcode) }
|
||||
|
||||
subject { described_class.search(search_term) }
|
||||
|
||||
context 'shortcode is exact' do
|
||||
let(:shortcode) { 'blobpats' }
|
||||
let(:search_term) { 'blobpats' }
|
||||
|
||||
it 'finds emoji' do
|
||||
is_expected.to include(custom_emoji)
|
||||
end
|
||||
end
|
||||
|
||||
context 'shortcode is partial' do
|
||||
let(:shortcode) { 'blobpats' }
|
||||
let(:search_term) { 'blob' }
|
||||
|
||||
it 'finds emoji' do
|
||||
is_expected.to include(custom_emoji)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#local?' do
|
||||
let(:custom_emoji) { Fabricate(:custom_emoji, domain: domain) }
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user