diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2020-07-08 15:01:29 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2020-07-08 15:01:29 +0200 |
| commit | bbcf48825bec60dab7a4de955bc4831eadb37792 (patch) | |
| tree | 79895748a2712e38ecfc96ad2d2630f392c4e851 /animism-align | |
| parent | 084d11a05fe1220c5104ea59f4fab4860e70b434 (diff) | |
media form and anuvva migration
Diffstat (limited to 'animism-align')
| -rw-r--r-- | animism-align/cli/app/sql/models/media.py | 2 | ||||
| -rw-r--r-- | animism-align/cli/app/sql/versions/202007081459_add_text.py | 29 | ||||
| -rw-r--r-- | animism-align/frontend/store.js | 2 | ||||
| -rw-r--r-- | animism-align/frontend/views/index.js | 1 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/components/media.form.js | 190 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/components/media.indexOptions.js | 61 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/components/media.menu.js | 49 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/containers/media.edit.js | 57 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/containers/media.index.js (renamed from animism-align/frontend/views/media/components/media.index.js) | 17 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/containers/media.new.js | 48 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/media.container.js | 12 | ||||
| -rw-r--r-- | animism-align/frontend/views/media/media.css | 4 |
12 files changed, 462 insertions, 10 deletions
diff --git a/animism-align/cli/app/sql/models/media.py b/animism-align/cli/app/sql/models/media.py index 8795e70..e3a395d 100644 --- a/animism-align/cli/app/sql/models/media.py +++ b/animism-align/cli/app/sql/models/media.py @@ -22,6 +22,7 @@ class Media(Base): date = Column(String(256, convert_unicode=True), nullable=True) source = Column(String(256, convert_unicode=True), nullable=True) medium = Column(String(64, convert_unicode=True), nullable=True) + description = Column(Text(convert_unicode=True), nullable=True) start_ts = Column(Float, nullable=True) settings = Column(JSON, default={}, nullable=True) @@ -38,6 +39,7 @@ class Media(Base): 'source': self.source, 'author': self.author, 'medium': self.medium, + 'description': self.description, 'start_ts': self.start_ts, 'settings': self.settings, } diff --git a/animism-align/cli/app/sql/versions/202007081459_add_text.py b/animism-align/cli/app/sql/versions/202007081459_add_text.py new file mode 100644 index 0000000..51f8020 --- /dev/null +++ b/animism-align/cli/app/sql/versions/202007081459_add_text.py @@ -0,0 +1,29 @@ +"""add text + +Revision ID: 65b17598c815 +Revises: d001c45d21a4 +Create Date: 2020-07-08 14:59:59.245875 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc + + +# revision identifiers, used by Alembic. +revision = '65b17598c815' +down_revision = 'd001c45d21a4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('media', sa.Column('description', sa.Text(_expect_unicode=True), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('media', 'description') + # ### end Alembic commands ### diff --git a/animism-align/frontend/store.js b/animism-align/frontend/store.js index 24542ff..5854eee 100644 --- a/animism-align/frontend/store.js +++ b/animism-align/frontend/store.js @@ -11,6 +11,7 @@ import audioReducer from './views/audio/audio.reducer' import paragraphReducer from './views/paragraph/paragraph.reducer' import annotationReducer from './views/annotation/annotation.reducer' import siteReducer from './views/site/site.reducer' +import mediaReducer from './views/media/media.reducer' // import collectionReducer from './views/collection/collection.reducer' const createRootReducer = history => ( @@ -23,6 +24,7 @@ const createRootReducer = history => ( audio: audioReducer, paragraph: paragraphReducer, annotation: annotationReducer, + media: mediaReducer, }) ) diff --git a/animism-align/frontend/views/index.js b/animism-align/frontend/views/index.js index b7b633e..2c9ee78 100644 --- a/animism-align/frontend/views/index.js +++ b/animism-align/frontend/views/index.js @@ -1,3 +1,4 @@ export { default as align } from './align/align.container' export { default as paragraph } from './paragraph/paragraph.container' export { default as upload } from './upload/upload.container' +export { default as media } from './media/media.container' diff --git a/animism-align/frontend/views/media/components/media.form.js b/animism-align/frontend/views/media/components/media.form.js new file mode 100644 index 0000000..ed96e6e --- /dev/null +++ b/animism-align/frontend/views/media/components/media.form.js @@ -0,0 +1,190 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' + +import { session } from '../../../session' + +import { TextInput, LabelDescription, TextArea, Checkbox, SubmitButton, Loader } from '../../../common' + +const newMedia = () => ({ + type: '', + tag: '', + url: '', + title: '', + author: '', + pre_title: '', + translated_title: '', + date: '', + source: '', + medium: '', + start_ts: 0, + settings: {}, +}) + +export default class MediaForm extends Component { + state = { + title: "", + submitTitle: "", + data: { ...newMedia() }, + errorFields: new Set([]), + } + + componentDidMount() { + const { data, isNew } = this.props + const title = isNew ? 'New media' : 'Editing ' + data.title + const submitTitle = isNew ? "Add Media" : "Save Changes" + this.setState({ + title, + submitTitle, + errorFields: new Set([]), + data: { + ...newMedia(), + ...data + }, + }) + } + + handleChange(e) { + const { errorFields } = this.state + const { name, value } = e.target + if (errorFields.has(name)) { + errorFields.delete(name) + } + let sanitizedValue = value + if (name === 'path') { + sanitizedValue = sanitizedValue.toLowerCase().replace(/ /, '-').replace(/[!@#$%^&*()[\]{}]/, '-').replace(/-+/, '-') + } + this.setState({ + errorFields, + data: { + ...this.state.data, + [name]: sanitizedValue, + } + }) + } + + handleSelect(name, value) { + const { errorFields } = this.state + if (errorFields.has(name)) { + errorFields.delete(name) + } + this.setState({ + errorFields, + data: { + ...this.state.data, + [name]: value, + } + }) + } + + handleSubmit(e) { + e.preventDefault() + const { isNew, onSubmit } = this.props + const { data } = this.state + const requiredKeys = "title".split(" ") + const validKeys = "title".split(" ") + const validData = validKeys.reduce((a,b) => { a[b] = data[b]; return a }, {}) + const errorFields = requiredKeys.filter(key => !validData[key]) + if (errorFields.length) { + console.log('error', errorFields, validData) + this.setState({ errorFields: new Set(errorFields) }) + } else { + if (isNew) { + // side effect: set username if we're creating a new media + // session.set('username', data.username) + } else { + validData.id = data.id + } + console.log('submit', validData) + onSubmit(validData) + } + } + + render() { + const { isNew } = this.props + const { title, submitTitle, errorFields, data } = this.state + /* + type: '', + tag: '', + url: '', + */ + return ( + <div className='form'> + <h1>{title}</h1> + <form onSubmit={this.handleSubmit.bind(this)}> + <TextInput + title="Author" + name="author" + required + data={data} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextInput + title="Title" + name="title" + required + data={data} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextInput + title="Title Prefix" + name="pre_title" + required + data={data} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextInput + title="Translated Title" + name="translated_title" + required + data={data} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextInput + title="Date" + name="date" + required + data={data} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextInput + title="Medium" + name="medium" + required + data={data} + onChange={this.handleChange.bind(this)} + /> + <TextInput + title="Source" + name="source" + placeholder="Courtesy of / Copyright" + required + data={data} + onChange={this.handleChange.bind(this)} + autoComplete="off" + /> + <TextArea + title="Description" + name="description" + data={data} + onChange={this.handleChange.bind(this)} + /> + <SubmitButton + title={submitTitle} + onClick={this.handleSubmit.bind(this)} + /> + {!!errorFields.size && + <label> + <span></span> + <span>Please complete the required fields =)</span> + </label> + } + </form> + </div> + ) + } +} diff --git a/animism-align/frontend/views/media/components/media.indexOptions.js b/animism-align/frontend/views/media/components/media.indexOptions.js new file mode 100644 index 0000000..774bf22 --- /dev/null +++ b/animism-align/frontend/views/media/components/media.indexOptions.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' + +import actions from '../../../actions' + +import { Select, Checkbox } from '../../../common' + +const thumbnailOptions = [ + { name: 'th', label: 'Thumbnails', }, + { name: 'sm', label: 'Small', }, + { name: 'md', label: 'Medium', }, + { name: 'lg', label: 'Large', }, + { name: 'orig', label: 'Original', }, +] + +const sortOptions = [ + { name: 'id-asc', label: 'Most recent' }, + { name: 'id-desc', label: 'Oldest first' }, + { name: 'username-asc', label: 'Username (A-Z)' }, + { name: 'username-desc', label: 'Username (Z-A)' }, + // { name: '-asc', label: '' }, + // { name: '-desc', label: '' }, + // { name: '-asc', label: '' }, + // { name: '-desc', label: '' }, + // { name: '-asc', label: '' }, + // { name: '-desc', label: '' }, +] + +class IndexOptions extends Component { + render() { + const { options } = this.props + return ( + <div className='row menubar'> + <div /> + <Select + name={'sort'} + options={sortOptions} + selected={options.sort} + onChange={actions.upload.updateOption} + /> + <Select + name={'thumbnailSize'} + options={thumbnailOptions} + selected={options.thumbnailSize} + onChange={actions.upload.updateOption} + /> + </div> + ) + } +} + +const mapStateToProps = state => ({ + options: state.upload.options, +}) + +const mapDispatchToProps = dispatch => ({ +}) + +export default connect(mapStateToProps, mapDispatchToProps)(IndexOptions) diff --git a/animism-align/frontend/views/media/components/media.menu.js b/animism-align/frontend/views/media/components/media.menu.js new file mode 100644 index 0000000..3d7e86a --- /dev/null +++ b/animism-align/frontend/views/media/components/media.menu.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react' +import { Route, Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from '../../../store' +import actions from '../../../actions' +import { MenuButton, FileInput } from '../../../common' + +const mapStateToProps = state => ({ + media: state.media, +}) + +export default class MediaMenu extends Component { + render() { + return ( + <div className='menuButtons'> + <Route exact path='/media/:id/show/' component={MediaShowMenu} /> + <Route exact path='/media/:id/edit/' component={MediaEditMenu} /> + <Route exact path='/media/new/' component={MediaNewMenu} /> + <Route exact path='/media/' component={MediaIndexMenu} /> + </div> + ) + } +} + +const MediaIndexMenu = () => ([ + <MenuButton key='new' name="new" href="/media/new/" />, +]) + +const MediaShowMenu = connect(mapStateToProps)((props) => ([ + <MenuButton key='back' name="back" />, + <MenuButton key='edit' name="edit" href={"/media/" + props.match.params.id + "/edit/"} />, + <MenuButton key='delete' name="delete" onClick={() => { + const { res: media } = props.media.show + if (confirm("Really delete this media?")) { + actions.media.destroy(media).then(() => { + history.push('/media/') + }) + } + }} />, +])) + +const MediaNewMenu = (props) => ([ + <MenuButton key='back' name="back" />, +]) + +const MediaEditMenu = (props) => ([ + <MenuButton key='back' name="back" />, +]) diff --git a/animism-align/frontend/views/media/containers/media.edit.js b/animism-align/frontend/views/media/containers/media.edit.js new file mode 100644 index 0000000..8c353d9 --- /dev/null +++ b/animism-align/frontend/views/media/containers/media.edit.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from '../../../store' +import actions from '../../../actions' + +import { Loader } from '../../../common' + +import MediaForm from '../components/media.form' +import MediaMenu from '../components/media.menu' + +class MediaEdit extends Component { + componentDidMount() { + console.log(this.props.match.params.id) + actions.media.show(this.props.match.params.id) + } + + handleSubmit(data) { + actions.media.update(data) + .then(response => { + // response + console.log(response) + history.push('/media/') + }) + } + + render() { + const { show } = this.props.media + if (show.loading || !show.res) { + return ( + <div className='form'> + <Loader /> + </div> + ) + } + return ( + <div className='row formContainer'> + <MediaMenu /> + <MediaForm + data={show.res} + onSubmit={this.handleSubmit.bind(this)} + /> + </div> + ) + } +} + +const mapStateToProps = state => ({ + media: state.media, +}) + +const mapDispatchToProps = dispatch => ({ + // searchActions: bindActionCreators({ ...searchActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(MediaEdit) diff --git a/animism-align/frontend/views/media/components/media.index.js b/animism-align/frontend/views/media/containers/media.index.js index a4127f9..09ef6ca 100644 --- a/animism-align/frontend/views/media/components/media.index.js +++ b/animism-align/frontend/views/media/containers/media.index.js @@ -1,16 +1,18 @@ import React, { Component } from 'react' import { Link } from 'react-router-dom' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' import { formatDateTime } from '../../../util' import { MenuButton, SmallMenuButton, Loader } from '../../../common' import actions from '../../../actions' -import MediaIndexOptions from './media.indexOptions' -import MediaMenu from './media.menu' +import MediaIndexOptions from '../components/media.indexOptions' +import MediaMenu from '../components/media.menu' // const { result, collectionLookup } = this.props -export default class MediaIndex extends Component { +class MediaIndex extends Component { componentDidMount() { this.fetch(false) } @@ -96,3 +98,12 @@ const MediaItem = ({ data }) => { ) } +const mapStateToProps = state => ({ + media: state.media, +}) + +const mapDispatchToProps = dispatch => ({ + // uploadActions: bindActionCreators({ ...uploadActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(MediaIndex) diff --git a/animism-align/frontend/views/media/containers/media.new.js b/animism-align/frontend/views/media/containers/media.new.js new file mode 100644 index 0000000..88bf467 --- /dev/null +++ b/animism-align/frontend/views/media/containers/media.new.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react' +import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { history } from '../../../store' +import actions from '../../../actions' + +import MediaForm from '../components/media.form' +import MediaMenu from '../components/media.menu' + +class MediaNew extends Component { + handleSubmit(data) { + console.log(data) + actions.media.create(data) + .then(res => { + console.log(res) + if (res.res && res.res.id) { + history.push('/media/') + } + }) + .catch(err => { + console.error('error') + }) + } + + render() { + return ( + <div className='row formContainer'> + <MediaMenu /> + <MediaForm + isNew + data={{}} + onSubmit={this.handleSubmit.bind(this)} + /> + </div> + ) + } +} + +const mapStateToProps = state => ({ + media: state.media, +}) + +const mapDispatchToProps = dispatch => ({ + // searchActions: bindActionCreators({ ...searchActions }, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(MediaNew) diff --git a/animism-align/frontend/views/media/media.container.js b/animism-align/frontend/views/media/media.container.js index 280efd0..41fed4b 100644 --- a/animism-align/frontend/views/media/media.container.js +++ b/animism-align/frontend/views/media/media.container.js @@ -7,23 +7,23 @@ import './media.css' import actions from '../../actions' -import MediaIndex from './components/media.index' -// import MediaShow from './components/media.show' -// import MediaNew from './components/media.new' -// import MediaEdit from './components/media.edit' +import MediaIndex from './containers/media.index' +// import MediaShow from './containers/media.show' +import MediaNew from './containers/media.new' +import MediaEdit from './containers/media.edit' class Container extends Component { render() { return ( <div className='media'> + <Route exact path='/media/:id/edit/' component={MediaEdit} /> + <Route exact path='/media/new/' component={MediaNew} /> <Route exact path='/media/' component={MediaIndex} /> </div> ) } } /* - <Route exact path='/media/:id/edit/' component={MediaEdit} /> - <Route exact path='/media/new/' component={MediaNew} /> <Route exact path='/media/:id/show/' component={MediaShow} /> */ const mapStateToProps = state => ({ diff --git a/animism-align/frontend/views/media/media.css b/animism-align/frontend/views/media/media.css index 56cf2fe..2f3ca0d 100644 --- a/animism-align/frontend/views/media/media.css +++ b/animism-align/frontend/views/media/media.css @@ -1 +1,3 @@ -* {}
\ No newline at end of file +.formContainer { + padding-top: 1rem; +}
\ No newline at end of file |
