@@ -1,4 +1,4 @@ | |||
import api from '../api' | |||
import api from '../api'; | |||
import { updateTimeline } from './timelines'; | |||
@@ -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 | |||
}; | |||
}; |
@@ -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 ( | |||
@@ -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({ | |||
<Route path='follow_requests' component={FollowRequests} /> | |||
<Route path='blocks' component={Blocks} /> | |||
<Route path='report' component={Report} /> | |||
<Route path='*' component={GenericNotFound} /> | |||
</Route> | |||
@@ -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)); | |||
} | |||
}); | |||
@@ -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 ( | |||
<div className='account__action-bar'> | |||
<div style={outerDropdownStyle}> | |||
@@ -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} | |||
/> | |||
</div> | |||
); | |||
@@ -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)); | |||
} | |||
}); | |||
@@ -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 ( | |||
<div className='status-check-box' style={{ display: 'flex' }}> | |||
<div | |||
className='status__content' | |||
style={{ flex: '1 1 auto', padding: '10px' }} | |||
dangerouslySetInnerHTML={content} | |||
/> | |||
<div style={{ flex: '0 0 auto', padding: '10px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> | |||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} /> | |||
</div> | |||
</div> | |||
); | |||
} | |||
}); | |||
export default StatusCheckBox; |
@@ -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); |
@@ -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 ( | |||
<Column heading={intl.formatMessage(messages.heading)} icon='flag'> | |||
<ColumnBackButtonSlim /> | |||
<div className='report' style={{ display: 'flex', flexDirection: 'column', maxHeight: '100%', boxSizing: 'border-box' }}> | |||
<div className='report__target' style={{ flex: '0 0 auto', padding: '10px' }}> | |||
<FormattedMessage id='report.target' defaultMessage='Reporting' /> | |||
<strong>{account.get('acct')}</strong> | |||
</div> | |||
<div style={{ flex: '1 1 auto' }} className='scrollable'> | |||
<div> | |||
{statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)} | |||
</div> | |||
</div> | |||
<div style={{ flex: '0 0 160px', padding: '10px' }}> | |||
<textarea | |||
className='report__textarea' | |||
placeholder={intl.formatMessage(messages.placeholder)} | |||
value={comment} | |||
onChange={this.handleCommentChange} | |||
style={textareaStyle} | |||
disabled={isSubmitting} | |||
/> | |||
<div style={{ marginTop: '10px', overflow: 'hidden' }}> | |||
<div style={{ float: 'right' }}><Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} /></div> | |||
</div> | |||
</div> | |||
</div> | |||
</Column> | |||
); | |||
} | |||
}); | |||
export default connect(makeMapStateToProps)(injectIntl(Report)); |
@@ -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 ( | |||
@@ -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 => <StatusContainer key={id} id={id} />); | |||
}, | |||
@@ -123,7 +128,7 @@ const Status = React.createClass({ | |||
{ancestors} | |||
<DetailedStatus status={status} me={me} onOpenMedia={this.handleOpenMedia} /> | |||
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} /> | |||
<ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} /> | |||
{descendants} | |||
</div> | |||
@@ -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 | |||
}); |
@@ -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; | |||
} | |||
}; |
@@ -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); | |||
} | |||
} |
@@ -93,6 +93,7 @@ code { | |||
width: 100%; | |||
outline: 0; | |||
font-family: inherit; | |||
resize: vertical; | |||
&:invalid { | |||
box-shadow: none; | |||
@@ -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 |
@@ -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 |
@@ -0,0 +1,2 @@ | |||
collection @reports | |||
extends 'api/v1/reports/show' |
@@ -0,0 +1,2 @@ | |||
object @report | |||
attributes :id, :action_taken |
@@ -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 | |||
@@ -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 |
@@ -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" | |||
@@ -0,0 +1,4 @@ | |||
Fabricator(:report) do | |||
comment "You nasty" | |||
action_taken false | |||
end |
@@ -0,0 +1,5 @@ | |||
require 'rails_helper' | |||
RSpec.describe Report, type: :model do | |||
end |