diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index f87518751..03aae885e 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -1,4 +1,4 @@
-import api from '../api'
+import api from '../api';
import { updateTimeline } from './timelines';
diff --git a/app/assets/javascripts/components/actions/reports.jsx b/app/assets/javascripts/components/actions/reports.jsx
new file mode 100644
index 000000000..2c1245dc4
--- /dev/null
+++ b/app/assets/javascripts/components/actions/reports.jsx
@@ -0,0 +1,64 @@
+import api from '../api';
+
+export const REPORT_INIT = 'REPORT_INIT';
+export const REPORT_CANCEL = 'REPORT_CANCEL';
+
+export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
+export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
+export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
+
+export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
+
+export function initReport(account, status) {
+ return {
+ type: REPORT_INIT,
+ account,
+ status
+ };
+};
+
+export function cancelReport() {
+ return {
+ type: REPORT_CANCEL
+ };
+};
+
+export function toggleStatusReport(statusId, checked) {
+ return {
+ type: REPORT_STATUS_TOGGLE,
+ statusId,
+ checked,
+ };
+};
+
+export function submitReport() {
+ return (dispatch, getState) => {
+ dispatch(submitReportRequest());
+
+ api(getState).post('/api/v1/reports', {
+ account_id: getState().getIn(['reports', 'new', 'account_id']),
+ status_ids: getState().getIn(['reports', 'new', 'status_ids']),
+ comment: getState().getIn(['reports', 'new', 'comment'])
+ }).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error)));
+ };
+};
+
+export function submitReportRequest() {
+ return {
+ type: REPORT_SUBMIT_REQUEST
+ };
+};
+
+export function submitReportSuccess(report) {
+ return {
+ type: REPORT_SUBMIT_SUCCESS,
+ report
+ };
+};
+
+export function submitReportFail(error) {
+ return {
+ type: REPORT_SUBMIT_FAIL,
+ error
+ };
+};
diff --git a/app/assets/javascripts/components/components/status_action_bar.jsx b/app/assets/javascripts/components/components/status_action_bar.jsx
index f2cc1fb12..35c458b5e 100644
--- a/app/assets/javascripts/components/components/status_action_bar.jsx
+++ b/app/assets/javascripts/components/components/status_action_bar.jsx
@@ -11,7 +11,8 @@ const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
- open: { id: 'status.open', defaultMessage: 'Expand' }
+ open: { id: 'status.open', defaultMessage: 'Expand' },
+ report: { id: 'status.report', defaultMessage: 'Report' }
});
const StatusActionBar = React.createClass({
@@ -27,7 +28,10 @@ const StatusActionBar = React.createClass({
onReblog: React.PropTypes.func,
onDelete: React.PropTypes.func,
onMention: React.PropTypes.func,
- onBlock: React.PropTypes.func
+ onBlock: React.PropTypes.func,
+ onReport: React.PropTypes.func,
+ me: React.PropTypes.number.isRequired,
+ intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
@@ -60,6 +64,11 @@ const StatusActionBar = React.createClass({
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
},
+ handleReport () {
+ this.props.onReport(this.props.status);
+ this.context.router.push('/report');
+ },
+
render () {
const { status, me, intl } = this.props;
let menu = [];
@@ -71,6 +80,7 @@ const StatusActionBar = React.createClass({
} else {
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.block), action: this.handleBlockClick });
+ menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport });
}
return (
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index e23c65121..ebef5c81b 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -34,6 +34,7 @@ import FollowRequests from '../features/follow_requests';
import GenericNotFound from '../features/generic_not_found';
import FavouritedStatuses from '../features/favourited_statuses';
import Blocks from '../features/blocks';
+import Report from '../features/report';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import de from 'react-intl/locale-data/de';
@@ -131,6 +132,7 @@ const Mastodon = React.createClass({
+
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index f5fb09d52..fc096a375 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -13,6 +13,7 @@ import {
} from '../actions/interactions';
import { blockAccount } from '../actions/accounts';
import { deleteStatus } from '../actions/statuses';
+import { initReport } from '../actions/reports';
import { openMedia } from '../actions/modal';
import { createSelector } from 'reselect'
import { isMobile } from '../is_mobile'
@@ -97,6 +98,10 @@ const mapDispatchToProps = (dispatch) => ({
onBlock (account) {
dispatch(blockAccount(account.get('id')));
+ },
+
+ onReport (status) {
+ dispatch(initReport(status.get('account'), status));
}
});
diff --git a/app/assets/javascripts/components/features/account/components/action_bar.jsx b/app/assets/javascripts/components/features/account/components/action_bar.jsx
index fe110954d..a2ab8172b 100644
--- a/app/assets/javascripts/components/features/account/components/action_bar.jsx
+++ b/app/assets/javascripts/components/features/account/components/action_bar.jsx
@@ -11,7 +11,8 @@ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
block: { id: 'account.block', defaultMessage: 'Block' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
- block: { id: 'account.block', defaultMessage: 'Block' }
+ block: { id: 'account.block', defaultMessage: 'Block' },
+ report: { id: 'account.report', defaultMessage: 'Report' }
});
const outerDropdownStyle = {
@@ -32,7 +33,9 @@ const ActionBar = React.createClass({
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func,
onBlock: React.PropTypes.func.isRequired,
- onMention: React.PropTypes.func.isRequired
+ onMention: React.PropTypes.func.isRequired,
+ onReport: React.PropTypes.func.isRequired,
+ intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
@@ -54,6 +57,10 @@ const ActionBar = React.createClass({
menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
}
+ if (account.get('id') !== me) {
+ menu.push({ text: intl.formatMessage(messages.report), action: this.props.onReport });
+ }
+
return (
diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
index ff3e8af2d..0cdfc8b02 100644
--- a/app/assets/javascripts/components/features/account_timeline/components/header.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
@@ -13,7 +13,8 @@ const Header = React.createClass({
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired,
onBlock: React.PropTypes.func.isRequired,
- onMention: React.PropTypes.func.isRequired
+ onMention: React.PropTypes.func.isRequired,
+ onReport: React.PropTypes.func.isRequired
},
mixins: [PureRenderMixin],
@@ -30,6 +31,11 @@ const Header = React.createClass({
this.props.onMention(this.props.account, this.context.router);
},
+ handleReport () {
+ this.props.onReport(this.props.account);
+ this.context.router.push('/report');
+ },
+
render () {
const { account, me } = this.props;
@@ -50,6 +56,7 @@ const Header = React.createClass({
me={me}
onBlock={this.handleBlock}
onMention={this.handleMention}
+ onReport={this.handleReport}
/>
);
diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
index dca826596..e4ce905fe 100644
--- a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
@@ -8,6 +8,7 @@ import {
unblockAccount
} from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose';
+import { initReport } from '../../../actions/reports';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
@@ -39,6 +40,10 @@ const mapDispatchToProps = dispatch => ({
onMention (account, router) {
dispatch(mentionCompose(account, router));
+ },
+
+ onReport (account) {
+ dispatch(initReport(account));
}
});
diff --git a/app/assets/javascripts/components/features/report/components/status_check_box.jsx b/app/assets/javascripts/components/features/report/components/status_check_box.jsx
new file mode 100644
index 000000000..df4a31457
--- /dev/null
+++ b/app/assets/javascripts/components/features/report/components/status_check_box.jsx
@@ -0,0 +1,38 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import emojify from '../../../emoji';
+import Toggle from 'react-toggle';
+
+const StatusCheckBox = React.createClass({
+
+ propTypes: {
+ status: ImmutablePropTypes.map.isRequired,
+ checked: React.PropTypes.bool,
+ onToggle: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool
+ },
+
+ mixins: [PureRenderMixin],
+
+ render () {
+ const { status, checked, onToggle, disabled } = this.props;
+ const content = { __html: emojify(status.get('content')) };
+
+ return (
+
+ );
+ }
+
+});
+
+export default StatusCheckBox;
diff --git a/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx b/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx
new file mode 100644
index 000000000..67ce9d9f3
--- /dev/null
+++ b/app/assets/javascripts/components/features/report/containers/status_check_box_container.jsx
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+import StatusCheckBox from '../components/status_check_box';
+import { toggleStatusReport } from '../../../actions/reports';
+import Immutable from 'immutable';
+
+const mapStateToProps = (state, { id }) => ({
+ status: state.getIn(['statuses', id]),
+ checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id)
+});
+
+const mapDispatchToProps = (dispatch, { id }) => ({
+
+ onToggle (e) {
+ dispatch(toggleStatusReport(id, e.target.checked));
+ }
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
diff --git a/app/assets/javascripts/components/features/report/index.jsx b/app/assets/javascripts/components/features/report/index.jsx
new file mode 100644
index 000000000..eb8d28fe8
--- /dev/null
+++ b/app/assets/javascripts/components/features/report/index.jsx
@@ -0,0 +1,130 @@
+import { connect } from 'react-redux';
+import { cancelReport, changeReportComment, submitReport } from '../../actions/reports';
+import { fetchAccountTimeline } from '../../actions/accounts';
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Column from '../ui/components/column';
+import Button from '../../components/button';
+import { makeGetAccount } from '../../selectors';
+import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
+import StatusCheckBox from './containers/status_check_box_container';
+import Immutable from 'immutable';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+
+const messages = defineMessages({
+ heading: { id: 'report.heading', defaultMessage: 'New report' },
+ placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
+ submit: { id: 'report.submit', defaultMessage: 'Submit' }
+});
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = state => {
+ const accountId = state.getIn(['reports', 'new', 'account_id']);
+
+ return {
+ isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
+ account: getAccount(state, accountId),
+ comment: state.getIn(['reports', 'new', 'comment']),
+ statusIds: state.getIn(['timelines', 'accounts_timelines', accountId, 'items'], Immutable.List())
+ };
+ };
+
+ return mapStateToProps;
+};
+
+const textareaStyle = {
+ marginBottom: '10px'
+};
+
+const Report = React.createClass({
+
+ contextTypes: {
+ router: React.PropTypes.object
+ },
+
+ propTypes: {
+ isSubmitting: React.PropTypes.bool,
+ account: ImmutablePropTypes.map,
+ statusIds: ImmutablePropTypes.list.isRequired,
+ comment: React.PropTypes.string.isRequired,
+ dispatch: React.PropTypes.func.isRequired,
+ intl: React.PropTypes.object.isRequired
+ },
+
+ mixins: [PureRenderMixin],
+
+ componentWillMount () {
+ if (!this.props.account) {
+ this.context.router.replace('/');
+ }
+ },
+
+ componentDidMount () {
+ if (!this.props.account) {
+ return;
+ }
+
+ this.props.dispatch(fetchAccountTimeline(this.props.account.get('id')));
+ },
+
+ componentWillReceiveProps (nextProps) {
+ if (this.props.account !== nextProps.account && nextProps.account) {
+ this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id')));
+ }
+ },
+
+ handleCommentChange (e) {
+ this.props.dispatch(changeReportComment(e.target.value));
+ },
+
+ handleSubmit () {
+ this.props.dispatch(submitReport());
+ this.context.router.replace('/');
+ },
+
+ render () {
+ const { account, comment, intl, statusIds, isSubmitting } = this.props;
+
+ if (!account) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {account.get('acct')}
+
+
+
+
+ {statusIds.map(statusId => )}
+
+
+
+
+
+
+ );
+ }
+
+});
+
+export default connect(makeMapStateToProps)(injectIntl(Report));
diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx
index 0e92acf55..cc4d5cca4 100644
--- a/app/assets/javascripts/components/features/status/components/action_bar.jsx
+++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx
@@ -9,7 +9,8 @@ const messages = defineMessages({
mention: { id: 'status.mention', defaultMessage: 'Mention' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
- favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }
+ favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
+ report: { id: 'status.report', defaultMessage: 'Report' }
});
const ActionBar = React.createClass({
@@ -25,6 +26,7 @@ const ActionBar = React.createClass({
onFavourite: React.PropTypes.func.isRequired,
onDelete: React.PropTypes.func.isRequired,
onMention: React.PropTypes.func.isRequired,
+ onReport: React.PropTypes.func,
me: React.PropTypes.number.isRequired,
intl: React.PropTypes.object.isRequired
},
@@ -51,6 +53,11 @@ const ActionBar = React.createClass({
this.props.onMention(this.props.status.get('account'), this.context.router);
},
+ handleReport () {
+ this.props.onReport(this.props.status);
+ this.context.router.push('/report');
+ },
+
render () {
const { status, me, intl } = this.props;
@@ -60,6 +67,7 @@ const ActionBar = React.createClass({
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
+ menu.push({ text: intl.formatMessage(messages.report), action: this.handleReport });
}
return (
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 894fa3176..e269bb661 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -14,6 +14,7 @@ import {
mentionCompose
} from '../../actions/compose';
import { deleteStatus } from '../../actions/statuses';
+import { initReport } from '../../actions/reports';
import {
makeGetStatus,
getStatusAncestors,
@@ -88,6 +89,10 @@ const Status = React.createClass({
this.props.dispatch(openMedia(media, index));
},
+ handleReport (status) {
+ this.props.dispatch(initReport(status.get('account'), status));
+ },
+
renderChildren (list) {
return list.map(id =>
);
},
@@ -123,7 +128,7 @@ const Status = React.createClass({
{ancestors}
-
+
{descendants}
diff --git a/app/assets/javascripts/components/reducers/index.jsx b/app/assets/javascripts/components/reducers/index.jsx
index 0798116c4..147030cca 100644
--- a/app/assets/javascripts/components/reducers/index.jsx
+++ b/app/assets/javascripts/components/reducers/index.jsx
@@ -14,6 +14,7 @@ import notifications from './notifications';
import settings from './settings';
import status_lists from './status_lists';
import cards from './cards';
+import reports from './reports';
export default combineReducers({
timelines,
@@ -30,5 +31,6 @@ export default combineReducers({
search,
notifications,
settings,
- cards
+ cards,
+ reports
});
diff --git a/app/assets/javascripts/components/reducers/reports.jsx b/app/assets/javascripts/components/reducers/reports.jsx
new file mode 100644
index 000000000..272aff3e5
--- /dev/null
+++ b/app/assets/javascripts/components/reducers/reports.jsx
@@ -0,0 +1,57 @@
+import {
+ REPORT_INIT,
+ REPORT_SUBMIT_REQUEST,
+ REPORT_SUBMIT_SUCCESS,
+ REPORT_SUBMIT_FAIL,
+ REPORT_CANCEL,
+ REPORT_STATUS_TOGGLE
+} from '../actions/reports';
+import Immutable from 'immutable';
+
+const initialState = Immutable.Map({
+ new: Immutable.Map({
+ isSubmitting: false,
+ account_id: null,
+ status_ids: Immutable.Set(),
+ comment: ''
+ })
+});
+
+export default function reports(state = initialState, action) {
+ switch(action.type) {
+ case REPORT_INIT:
+ return state.withMutations(map => {
+ map.setIn(['new', 'isSubmitting'], false);
+ map.setIn(['new', 'account_id'], action.account.get('id'));
+
+ if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
+ map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.get('id')]) : Immutable.Set());
+ map.setIn(['new', 'comment'], '');
+ } else {
+ map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.get('id')));
+ }
+ });
+ case REPORT_STATUS_TOGGLE:
+ return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => {
+ if (action.checked) {
+ return set.add(action.statusId);
+ }
+
+ return set.remove(action.statusId);
+ });
+ case REPORT_SUBMIT_REQUEST:
+ return state.setIn(['new', 'isSubmitting'], true);
+ case REPORT_SUBMIT_FAIL:
+ return state.setIn(['new', 'isSubmitting'], false);
+ case REPORT_CANCEL:
+ case REPORT_SUBMIT_SUCCESS:
+ return state.withMutations(map => {
+ map.setIn(['new', 'account_id'], null);
+ map.setIn(['new', 'status_ids'], Immutable.Set());
+ map.setIn(['new', 'comment'], '');
+ map.setIn(['new', 'isSubmitting'], false);
+ });
+ default:
+ return state;
+ }
+};
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 247ea3132..912405a9f 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -228,6 +228,14 @@ a.status__content__spoiler-link {
}
}
+.status-check-box {
+ border-bottom: 1px solid lighten($color1, 8%);
+
+ .status__content {
+ background: lighten($color1, 4%);
+ }
+}
+
.status__prepend {
margin-left: 68px;
color: lighten($color1, 26%);
@@ -1142,3 +1150,35 @@ button.active i.fa-retweet {
color: $color3;
}
+.report__target {
+ border-bottom: 1px solid lighten($color1, 4%);
+ color: $color2;
+ padding-bottom: 10px;
+
+ strong {
+ display: block;
+ color: $color5;
+ font-weight: 500;
+ }
+}
+
+.report__textarea {
+ background: transparent;
+ box-sizing: border-box;
+ border: 0;
+ border-bottom: 2px solid $color3;
+ border-radius: 2px 2px 0 0;
+ padding: 7px 4px;
+ font-size: 14px;
+ color: $color5;
+ display: block;
+ width: 100%;
+ outline: 0;
+ font-family: inherit;
+ resize: vertical;
+
+ &:active, &:focus {
+ border-bottom-color: $color4;
+ background: rgba($color8, 0.1);
+ }
+}
diff --git a/app/assets/stylesheets/forms.scss b/app/assets/stylesheets/forms.scss
index a97a767e0..bc99b36a6 100644
--- a/app/assets/stylesheets/forms.scss
+++ b/app/assets/stylesheets/forms.scss
@@ -93,6 +93,7 @@ code {
width: 100%;
outline: 0;
font-family: inherit;
+ resize: vertical;
&:invalid {
box-shadow: none;
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
new file mode 100644
index 000000000..46bdddbc1
--- /dev/null
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class Api::V1::ReportsController < ApiController
+ before_action -> { doorkeeper_authorize! :read }, except: [:create]
+ before_action -> { doorkeeper_authorize! :write }, only: [:create]
+ before_action :require_user!
+
+ respond_to :json
+
+ def index
+ @reports = Report.where(account: current_account)
+ end
+
+ def create
+ status_ids = params[:status_ids].is_a?(Enumerable) ? params[:status_ids] : [params[:status_ids]]
+
+ @report = Report.create!(account: current_account,
+ target_account: Account.find(params[:account_id]),
+ status_ids: Status.find(status_ids).pluck(:id),
+ comment: params[:comment])
+
+ render :show
+ end
+end
diff --git a/app/models/report.rb b/app/models/report.rb
new file mode 100644
index 000000000..05dc8cff1
--- /dev/null
+++ b/app/models/report.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Report < ApplicationRecord
+ belongs_to :account
+ belongs_to :target_account, class_name: 'Account'
+
+ scope :unresolved, -> { where(action_taken: false) }
+ scope :resolved, -> { where(action_taken: true) }
+end
diff --git a/app/views/api/v1/reports/index.rabl b/app/views/api/v1/reports/index.rabl
new file mode 100644
index 000000000..4f0794027
--- /dev/null
+++ b/app/views/api/v1/reports/index.rabl
@@ -0,0 +1,2 @@
+collection @reports
+extends 'api/v1/reports/show'
diff --git a/app/views/api/v1/reports/show.rabl b/app/views/api/v1/reports/show.rabl
new file mode 100644
index 000000000..006db51e3
--- /dev/null
+++ b/app/views/api/v1/reports/show.rabl
@@ -0,0 +1,2 @@
+object @report
+attributes :id, :action_taken
diff --git a/config/routes.rb b/config/routes.rb
index 3da7563fd..7f55413ee 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -115,6 +115,7 @@ Rails.application.routes.draw do
resources :apps, only: [:create]
resources :blocks, only: [:index]
resources :favourites, only: [:index]
+ resources :reports, only: [:index, :create]
resources :follow_requests, only: [:index] do
member do
diff --git a/db/migrate/20170214110202_create_reports.rb b/db/migrate/20170214110202_create_reports.rb
new file mode 100644
index 000000000..aa772803b
--- /dev/null
+++ b/db/migrate/20170214110202_create_reports.rb
@@ -0,0 +1,13 @@
+class CreateReports < ActiveRecord::Migration[5.0]
+ def change
+ create_table :reports do |t|
+ t.integer :account_id, null: false
+ t.integer :target_account_id, null: false
+ t.integer :status_ids, array: true, null: false, default: []
+ t.text :comment, null: false, default: ''
+ t.boolean :action_taken, null: false, default: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 86a05ebe6..8fa0c056c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170209184350) do
+ActiveRecord::Schema.define(version: 20170214110202) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -173,6 +173,16 @@ ActiveRecord::Schema.define(version: 20170209184350) do
t.index ["status_id"], name: "index_preview_cards_on_status_id", unique: true, using: :btree
end
+ create_table "reports", force: :cascade do |t|
+ t.integer "account_id", null: false
+ t.integer "target_account_id", null: false
+ t.integer "status_ids", default: [], null: false, array: true
+ t.text "comment", default: "", null: false
+ t.boolean "action_taken", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "settings", force: :cascade do |t|
t.string "var", null: false
t.text "value"
diff --git a/spec/fabricators/report_fabricator.rb b/spec/fabricators/report_fabricator.rb
new file mode 100644
index 000000000..b9fa360a7
--- /dev/null
+++ b/spec/fabricators/report_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:report) do
+ comment "You nasty"
+ action_taken false
+end
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
new file mode 100644
index 000000000..ade53cffa
--- /dev/null
+++ b/spec/models/report_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Report, type: :model do
+
+end