Commit bf72c77a authored by Yannis Barlas's avatar Yannis Barlas
Browse files

add editoria app folder

parent 426c68ee
## Editoria
### 1.1.4
* Fix for unsaved changes warning in Wax.
### 1.1.3
* New design for the book builder, the dashboard and the theme.
* Use new version of the Wax Editor. The editor can now accept configuration options, layouts and has been broken down into three separate modules (pubsweet integration, react integration and core) for better separation of concerns.
* Renamed 'remove' button to 'delete' for consistency with the bookbuilder.
* Fixed issue with fragments disappearing when uploading multiple files.
* Renamed 'Front Matter' and 'Back Matter' to 'Frontmatter' and 'Backmatter'.
* Double clicking on a book in the dashboard will take you to the book builder for that book, instead of opening the renaming interface.
* The position of 'Edit' and 'Rename' actions in the dashboard have been swapped ('Edit' now comes first).
* Books in the dashboard now appear in alphabetical order.
* Diacritics work within notes in Wax
### Editoria 1.0, July 2017
Editoria was started as a collaboration between UCP and Coko, initial funding came from the Mellon Foundation.
Those involved in conceptualising, designing, and building Editoria (including the Wax Editor) from 0 to 1.0 include:
* Erich van Rijn
* Catherine Mitchell
* Kate Warne
* Cindy Fulton
* Lisa Schiff
* Justin Gonder
* Juliana Froggatt
* Adam Hyde
* Kristen Ratan
* Yannis Barlas
* Christos Kokosias
* Julien Taquet
* Alex Theg
* Alexandros Georgantas
* Giannis Kopanas
MIT License
Copyright (c) 2017 University of California Press
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
## Overview
Editoria is a book production platform, built with [Pubsweet](https://gitlab.coko.foundation/pubsweet/) and [Substance](http://substance.io/).
This application is being developed by the [Coko Foundation](https://coko.foundation/), for the [University of California Press](http://www.ucpress.edu/).
## Requirements
Node >= 7.6
npm >= 3
## Installation
First you need to clone this repository on your machine.
```git clone git@gitlab.coko.foundation:editoria/editoria.git```
or the ```https``` equivalent:
```git clone https://gitlab.coko.foundation/editoria/editoria.git```
Once you have, navigate to the project's root directory.
```cd editoria```
This application requires node 7.6 or greater.
If you use nvm for managing different node versions, the project includes an ```.nvmrc``` file that you can take advantage of.
The application has been tested with node 7.9 .
Install the project's dependencies.
```npm install```
Create a database.
```npm run setupdb```
Follow the instructions, create the administrator user and name your first book.
Editoria uses [INK](https://gitlab.coko.foundation/INK/ink-api) in order to convert word files into our custom variant of HTML, [Editoria Typescript](https://gitlab.coko.foundation/XSweet/editoria_typescript).
For this to work, you need a valid username and password for a running INK instance on a server.
To use these credentials in Editoria, you need to pass the app three enviroment variables, namely `INK_ENDPOINT`, `INK_USERNAME` and `INK_PASSWORD`.
You can pass those in the `.env.production` file that you'll find in the project's root directory.
Please note that this file will be generated automatically the first time you run the `setupdb` command.
You'll find a sample configuration in the [.env.sample](https://gitlab.coko.foundation/editoria/editoria/blob/master/.env.sample) file in the root directory.
<!--- We provide default valid credentials already for demo purposes on our own INK instance. --->
<!--- If you simply want to try this out, you can uncomment the `INK_USERNAME`, `INK_PASSWORD` and `INK_ENDPOINT` environment variables in the `.env.production` file. --->
Once that is done, you can run the app like so:
```npm start```
## Set up
Log in as an administrator, and click on the "Teams" link in the navigation bar.
You can now assign different users to different roles.
If a user is a production editor, the user can then also edit user roles for all other users.
You're good to go!
import React from 'react'
import ReactDOM from 'react-dom'
import configureStore from 'pubsweet-client/src/store/configureStore'
import Root from 'pubsweet-client/src/components/Root'
import { AppContainer } from 'react-hot-loader'
import { browserHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
let store = configureStore(browserHistory, {})
let history = syncHistoryWithStore(browserHistory, store)
const rootEl = document.getElementById('root')
ReactDOM.render(
<AppContainer>
<Root store={store} history={history} />
</AppContainer>,
rootEl
)
if (module.hot) {
module.hot.accept('pubsweet-client/src/components/Root', () => {
const NextRoot = require('pubsweet-client/src/components/Root').default
ReactDOM.render(
<AppContainer>
<NextRoot store={store} history={history} />
</AppContainer>,
rootEl
)
})
}
function isOwner (user, object) {
if (!user) return false
if (object && object.owners) {
const owner = object.owners.find((o) => {
return o.id === user.id
})
if (owner) return true
}
return false
}
function belongsToTeam (user, team) {
const teams = user.teams.filter((t) => {
return t.teamType.name === team
})
return teams.length > 0
}
function isAuthor (user) {
return belongsToTeam(user, 'Author')
}
function isCopyEditor (user) {
return belongsToTeam(user, 'Copy Editor')
}
function isProductionEditor (user) {
return belongsToTeam(user, 'Production Editor')
}
function hasRightsOnCollection (user, collectionId) {
const collectionInTeams = user.teams.find((team) => {
const type = team.object.type
const id = team.object.id
return type === 'collection' && id === collectionId
})
if (collectionInTeams) return true
return false
}
// function isWorkflowIng (fragment, workflow) {
// if (fragment && fragment.progress) {
// if (workflow) {
// return fragment.progress[workflow] === 1
// }
// }
// return false
// }
const editoria = (user, operation, object) => {
if (!user) return false
if (user.admin) return true
if (operation === 'admin') return false
// object might be an array of objects (eg. teams, users, etc.)
// pick up if that is the case and use the first one to define the type of those objects
if (Array.isArray(object)) {
const list = object
let type
if (list.length > 0) type = list[0].type
if (type === 'user' || type === 'team') {
if (isProductionEditor(user)) {
if (operation === 'read') return true
if (operation === 'update' && type === 'team') return true
}
// remove when you can write to collection
// only here to get the collection's production editor
// edit: also here for the user to be able to read the teams
// TODO eventually the user should only get his teams
if ((isCopyEditor(user) || isAuthor(user)) && operation === 'read') return true
return false
}
// TODO -- handle array of collections
if (type === 'collection') {
return true
}
// TO DO -- can anyone manage an array of fragments
}
if (object.type === 'fragment') {
const fragment = object
const collectionId = fragment.book
if (operation === 'read') return true
if (!hasRightsOnCollection(user, collectionId)) return false
if (isProductionEditor(user)) return true
// TO DO -- what about create and delete?
if (operation === 'read' || operation === 'delete' || operation === 'create') return true
// temporarily here -- change when you see what the update was
if (operation === 'update') return true
// do not block the if flow, as someone might belong to more than one team
// if (operation === 'update') {
// if (isCopyEditor(user)) {
// const isEditing = isWorkflowIng(fragment, 'edit')
// if (isEditing) return true
// }
// if (isAuthor(user)) {
// const isReviewing = isWorkflowIng(fragment, 'review')
// if (isReviewing) return true
// }
// }
}
if (object.type === 'collection') {
const collection = object
const hasRights = hasRightsOnCollection(user, collection.id)
if (hasRights) {
if (isProductionEditor(user)) return true
if (isCopyEditor(user) || isAuthor(user)) {
// remove the create, this WILL NOT work for multiple collections
// simply here to bypass the fact that to create a fragment for the collection,
// you need to have create rights for the collection itself
if (operation === 'read' || operation === 'create') return true
}
}
if (isOwner(user, collection)) return true
}
if (object.type === 'user') {
return user.id === object.id
}
if (object.type === 'team') {
const team = object
const collectionId = team.object.id
if (!hasRightsOnCollection(user, collectionId)) return false
const permittedOperations = ['update', 'read']
if (!permittedOperations.includes(operation)) return false
if (isProductionEditor(user)) return true
return false
}
// console.log('all failed \n')
return false
}
module.exports = editoria
import React from 'react'
import { browserHistory } from 'react-router'
import { LinkContainer } from 'react-router-bootstrap'
import { Navbar, Nav, NavItem, NavbarBrand } from 'react-bootstrap'
import Authorize from 'pubsweet-client/src/helpers/Authorize'
import NavbarUser from 'pubsweet-component-navigation/NavbarUser'
// TODO -- break into smaller components
export default class Navigation extends React.Component {
constructor (props) {
super(props)
this.logout = this.logout.bind(this)
// rewrite cleaner
// should the manage component maybe pass the location prop?
browserHistory.listen(function (event) {
this.collectionId = ''
this.inEditor = event.pathname.match(/fragments/g)
if (this.inEditor) {
let pathnameSplited = event.pathname.split('/')
this.collectionId = pathnameSplited[2]
}
}.bind(this))
}
logout () {
const { logoutUser } = this.props.actions
logoutUser()
browserHistory.push('/login')
}
render () {
const { currentUser } = this.props
let logoutButtonIfAuthenticated
if (currentUser.isAuthenticated) {
logoutButtonIfAuthenticated = (
<NavbarUser
user={currentUser.user}
onLogoutClick={this.logout}
/>
)
}
let BackToBooks
if (this.inEditor) {
BackToBooks = (
<LinkContainer to={'/books/' + this.collectionId + '/book-builder'}>
<NavItem>Back to book</NavItem>
</LinkContainer>
)
}
// TODO -- fix object properties underneath
return (
<Navbar fluid>
<Navbar.Header>
<NavbarBrand>
<a href='#'>
Editoria
</a>
</NavbarBrand>
</Navbar.Header>
<Nav>
<LinkContainer to='/books'>
<NavItem>Books</NavItem>
</LinkContainer>
<Authorize operation='read' object='users'>
<LinkContainer to='/users'>
<NavItem>Users</NavItem>
</LinkContainer>
</Authorize>
<Authorize operation='read' object='teams'>
<LinkContainer to='/teams'>
<NavItem>Teams</NavItem>
</LinkContainer>
</Authorize>
{BackToBooks}
</Nav>
{ logoutButtonIfAuthenticated }
</Navbar>
)
}
}
Navigation.propTypes = {
actions: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="root"></div>
<script type="text/javascript" charset="utf-8" src="/assets/app.js"></script>
</body>
</html>
import React from 'react'
import { Redirect, Route } from 'react-router'
import { requireAuthentication } from 'pubsweet-client/src/components/AuthenticatedComponent'
// Manage
import Manage from 'pubsweet-component-manage/Manage'
import UsersManager from 'pubsweet-component-users-manager/UsersManager'
import TeamsManager from 'pubsweet-component-teams-manager/TeamsManager'
import Blog from 'pubsweet-component-blog/Blog'
// Authentication
import Login from 'pubsweet-component-login/Login'
import Signup from 'pubsweet-component-signup/Signup'
// Editor
import Wax from 'pubsweet-component-wax/src/WaxPubsweet'
import WithConfig from 'pubsweet-component-wax/src/WithConfig'
// Editoria
// import BookBuilder from './components/BookBuilder/BookBuilder'
import BookBuilder from 'pubsweet-component-bookbuilder/src/BookBuilder'
import Dashboard from 'pubsweet-component-editoria-dashboard/src/Dashboard'
// Pass configuration to editor
const Editor = WithConfig(Wax, {
layout: 'default',
lockWhenEditing: true
})
// FIXME: this shouldn't be using the collection as the object
const AuthenticatedManage = requireAuthentication(
Manage, 'create', state => state.collections
)
// TODO
// these two setups are a bit hacky, but they work
// leaving, as their components will most likely be removed completely soon
const AdminOnlyUsersManager = requireAuthentication(
UsersManager, 'admin', state => state.collections[0]
)
const AdminOnlyTeamsManager = requireAuthentication(
TeamsManager, 'admin', state => state.collections[0]
)
export default (
<Route>
<Redirect from='/' to='books' />
<Redirect from='/manage/posts' to='books' />
<Route path='/' component={AuthenticatedManage}>
<Route path='books' component={Dashboard} />
<Route path='blog' component={Blog} />
<Route path='books/:id/book-builder' component={BookBuilder} />
<Route path='books/:bookId/fragments/:fragmentId' component={Editor} />
<Route path='users' component={AdminOnlyUsersManager} />
<Route path='teams' component={AdminOnlyTeamsManager} />
</Route>
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Redirect path='*' to='books' />
</Route>
)
const universal = require('./universal')
module.exports = {
authsome: universal.authsome,
bookBuilder: universal.bookBuilder,
dashboard: universal.dashboard,
pubsweet: universal.pubsweet,
'pubsweet-client': universal.pubsweetClient,
'pubsweet-component-ink-backend': universal.inkBackend,
'pubsweet-server': universal.pubsweetServer,
validations: universal.validations
}
const universal = require('./universal')
module.exports = {
authsome: universal.authsome,
bookBuilder: universal.bookBuilder,
dashboard: universal.dashboard,
pubsweet: universal.pubsweet,
'pubsweet-client': universal.pubsweetClient,
'pubsweet-component-ink-backend': universal.inkBackend,
'pubsweet-server': universal.pubsweetServer,
validations: universal.validations
}
const Joi = require('joi')
const path = require('path')
const editoriaMode = require('../app/authsome_editoria')
const inkEndpoint = process.env.INK_ENDPOINT
const inkUsername = process.env.INK_USERNAME
const inkPassword = process.env.INK_PASSWORD
const teams = {
teamProduction: {
name: 'Production Editor',
permissions: 'all'
},
teamCopyEditor: {
name: 'Copy Editor',
permissions: 'update'
},
teamauthors: {
name: 'Author',
permissions: 'update'
}
}
module.exports = {
authsome: {
mode: editoriaMode,
teams
},
bookBuilder: {
chapter: {
dropdownValues: {
front: [
'Table of Contents',
'Introduction',
'Preface',
'Preface 1',
'Preface 2',
'Preface 3',
'Preface 4',
'Preface 5',
'Preface 6',
'Preface 7',
'Preface 8',
'Preface 9',
'Preface 10'
],
back: [
'Appendix A',
'Appendix B',
'Appendix C'
]
}
},
teamTypes: teams
},
dashboard: {
teamTypes: teams
},
inkBackend: {
inkEndpoint: inkEndpoint || 'http://ink-api.coko.foundation',
email: inkUsername,
password: inkPassword,
maxRetries: 60
},