From bdefeeb55ade3be24107408a99699bf3292b6513 Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Wed, 29 Jun 2016 21:10:32 -0700 Subject: [PATCH 01/10] highlight selection/deletion added --- app/actions/actionTypes.js | 1 + app/actions/actions.js | 5 +++ app/components/annotation/Article.js | 56 +++++++++++++++++++++++++++- app/reducers/articleReducers.js | 9 ++++- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/app/actions/actionTypes.js b/app/actions/actionTypes.js index 26e4932..6ed680b 100644 --- a/app/actions/actionTypes.js +++ b/app/actions/actionTypes.js @@ -2,3 +2,4 @@ export const ADD_HIGHLIGHT = 'ADD_HIGHLIGHT'; export const ACTIVATE_TOPIC = 'ACTIVATE_TOPIC'; export const NEW_ARTICLE = 'NEW_ARTICLE'; export const NEW_QUESTIONS = 'NEW_QUESTIONS'; +export const DELETE_HIGHLIGHT = 'DELETE_HIGHLIGHT'; diff --git a/app/actions/actions.js b/app/actions/actions.js index 56a5337..e89246a 100644 --- a/app/actions/actions.js +++ b/app/actions/actions.js @@ -4,6 +4,10 @@ export function addHighlight(start, end, selectedText) { return { type: types.ADD_HIGHLIGHT, selection: {start, end, selectedText} }; } +export function deleteHighlight(highlight) { + return { type: types.DELETE_HIGHLIGHT, highlight: highlight }; +} + export function activateTopic(topic) { return { type: types.ACTIVATE_TOPIC, topic }; } @@ -17,3 +21,4 @@ export function newArticle(article) { export function newQuestions(questions) { return { type: types.NEW_QUESTIONS, questions }; } + diff --git a/app/components/annotation/Article.js b/app/components/annotation/Article.js index 7e8123f..e73a3ac 100644 --- a/app/components/annotation/Article.js +++ b/app/components/annotation/Article.js @@ -1,6 +1,7 @@ import React from 'react'; -import { addHighlight } from 'actions/actions'; +import { addHighlight, deleteHighlight } from 'actions/actions'; import { connect } from 'react-redux'; +import jquery from 'jquery'; import 'Article.scss'; @@ -8,6 +9,9 @@ const mapDispatchToProps = dispatch => { return { onHighlight: (start, end, selectedText) => { dispatch(addHighlight(start, end, selectedText)); + }, + removeHighlight: (highlight) => { + dispatch(deleteHighlight(highlight)); } }; } @@ -28,10 +32,16 @@ const Article = React.createClass({ article: React.PropTypes.object.isRequired, topics: React.PropTypes.array.isRequired, onHighlight: React.PropTypes.func, + removeHighlight: React.PropTypes.func, + selectedHighlight: React.PropTypes.object, highlights: React.PropTypes.array, currentTopic: React.PropTypes.string }, + getInitialState: function() { + return { selectedHighlight: null }; + }, + getOffset: function(childNodes, targetNode) { // since we're splitting
into s we'll need to find which // anchorOffset is referring to, and find that offset from the start of
@@ -70,6 +80,37 @@ const Article = React.createClass({ } }, + componentDidMount: function() { + // unsure if jquery is necessary to mount keypress handler + // but this is what I found and it seems to work + var $ = jquery; + $(document.body).on('keydown', this.handleKeyDown); + }, + + componentWillUnmount: function() { + var $ = jquery; + $(document.body).off('keydown', this.handleKeyDown); + }, + + handleKeyDown: function(e) { + if (e.keyCode === 8 || e.keyCode === 46) { + e.preventDefault(); + if (this.state.selectedHighlight) { + this.props.removeHighlight(this.state.selectedHighlight); + } + } + }, + + selectHighlight: function(highlight) { + if (this.state.selectedHighlight + && highlight.start === this.state.selectedHighlight.start + && highlight.end === this.state.selectedHighlight.end) { + this.setState({ selectedHighlight: null }); + } else { + this.setState({ selectedHighlight: highlight }); + } + }, + render() { const {topic_id}: string = this.context.params let topic = this.props.topics[topic_id]; @@ -100,9 +141,20 @@ const Article = React.createClass({ return ({text.substring(start, curHL.start)}); } else { // render highlight + // might want to create general method to check for object equality start = curHL.end; + var classStr = 'highlighted'; + if (this.state.selectedHighlight + && curHL.start === this.state.selectedHighlight.start + && curHL.end === this.state.selectedHighlight.end) { + classStr += ' selected' + this.state.selectedHighlight.topic; + } else { + classStr += ' topic' + curHL.topic; + } + return ( { this.selectHighlight(curHL) } } + className={ classStr } >{text.substring(curHL.start, curHL.end)}); } })} diff --git a/app/reducers/articleReducers.js b/app/reducers/articleReducers.js index 2d05a41..b28c0cf 100644 --- a/app/reducers/articleReducers.js +++ b/app/reducers/articleReducers.js @@ -1,4 +1,5 @@ import { ADD_HIGHLIGHT, + DELETE_HIGHLIGHT, NEW_ARTICLE, ACTIVATE_TOPIC } from '../actions/actionTypes'; import api from '../api.js'; @@ -26,7 +27,7 @@ function mergeHighlights(list) { for (var j = i + 1; j < n; j++) { if (list[i].end >= list[j].start) { newrange.text += list[j].text.substring( - Math.min(newrange.end, list[j].end) - list[j].start, list[j].end + Math.min(newrange.end, list[j].end) - list[j].start, list[j].text.length ); newrange.end = Math.max(list[j].end, newrange.end); continue; @@ -42,6 +43,7 @@ function mergeHighlights(list) { export default function articleReducer(state = initialState, action) { switch (action.type) { + case ADD_HIGHLIGHT: var newHighlights = state.highlights.concat( { start: action.selection.start, @@ -70,6 +72,11 @@ export default function articleReducer(state = initialState, action) { curArticle: state.curArticle + 1 }); case ACTIVATE_TOPIC: return Object.assign({}, state, { currentTopic: action.topic }); + case DELETE_HIGHLIGHT: + var index = state.highlights.indexOf(action.highlight); + var arr1 = state.highlights.slice(0, index); + var arr2 = state.highlights.slice(index + 1); + return Object.assign({}, state, { highlights: arr1.concat(arr2) }); default: return state; } From 2ae9576b1e63843b730c0b13eb35c616164094c1 Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Tue, 5 Jul 2016 11:22:54 -0700 Subject: [PATCH 02/10] test --- app/components/annotation/Article.js | 2 +- app/styles/Article.scss | 14 ++++++++++++++ app/styles/_styleguide.scss | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/components/annotation/Article.js b/app/components/annotation/Article.js index e73a3ac..1c2ff93 100644 --- a/app/components/annotation/Article.js +++ b/app/components/annotation/Article.js @@ -134,7 +134,7 @@ const Article = React.createClass({ Focus on the bold text about '{topic.name}' and answer the questions.
this.articleRef = ref} className='article' onClick={this.handleClick}> - {Array(highlights.length * 2).fill().map((_,i) => { + {Array(highlights.length * 2).fill().map((_, i) => { var curHL = highlights[i / 2 | 0]; if (i % 2 === 0) { // render normal text diff --git a/app/styles/Article.scss b/app/styles/Article.scss index f72d9d1..a75d7b1 100644 --- a/app/styles/Article.scss +++ b/app/styles/Article.scss @@ -7,6 +7,7 @@ line-height: 1.5; font-family: $font-newspaper; } +$darken-highlight: 10%; .text-wrapper { max-width: 500px; @@ -21,18 +22,31 @@ .highlighted { // TODO: generate programmatically? background-color: $red_accent; + &.topic1 { background-color: $red_accent; } + &.selected1 { + background-color: darken($red_accent, $darken-highlight); + } &.topic2 { background-color: $yellow_accent; } + &.selected2 { + background-color: darken($yellow_accent, $darken-highlight); + } &.topic3 { background-color: $green_accent; } + &.selected3 { + background-color: darken($green_accent, $darken-highlight); + } &.topic4 { background-color: $blue_accent; } + &.selected4 { + background-color: darken($blue_accent, $darken-highlight); + } } /* react makes this not work */ diff --git a/app/styles/_styleguide.scss b/app/styles/_styleguide.scss index 48beb48..86c47cd 100644 --- a/app/styles/_styleguide.scss +++ b/app/styles/_styleguide.scss @@ -7,4 +7,4 @@ $offwhite : #ededed; $red_accent : #F16061; $yellow_accent : #FDD484; $green_accent : #AFD792; -$blue_accent : #a8d2bf; +$blue_accent : #A8D2BF; From eee2cfb13d838f548e5ab09e2bb73f9bae1bb736 Mon Sep 17 00:00:00 2001 From: Steven Elleman Date: Sun, 10 Jul 2016 11:12:58 -0700 Subject: [PATCH 03/10] clean and merged highlight --- app/actions/article.js | 10 ++ app/components/Article/index.js | 226 +++++++++++++++++++++++++++++++- app/reducers/article.js | 41 +++++- 3 files changed, 270 insertions(+), 7 deletions(-) diff --git a/app/actions/article.js b/app/actions/article.js index d784d82..fa7d1ec 100644 --- a/app/actions/article.js +++ b/app/actions/article.js @@ -2,11 +2,21 @@ export const ADD_HIGHLIGHT = 'ADD_HIGHLIGHT'; export const ACTIVATE_TOPIC = 'ACTIVATE_TOPIC'; export const NEW_QUESTIONS = 'NEW_QUESTIONS'; export const NEW_ARTICLE = 'NEW_ARTICLE'; +export const DELETE_HIGHLIGHT = 'DELETE_HIGHLIGHT'; +export const SELECT_HIGHLIGHT = 'SELECT_HIGHLIGHT'; export function addHighlight(start, end, selectedText) { return { type: ADD_HIGHLIGHT, selection: {start, end, selectedText} }; } +export function deleteHighlight(source) { + return { type: DELETE_HIGHLIGHT, highlights: source }; +} + +export function selectHighlight(source) { + return { type: SELECT_HIGHLIGHT, highlights: source }; +} + export function activateTopic(topic) { return { type: ACTIVATE_TOPIC, topic }; } diff --git a/app/components/Article/index.js b/app/components/Article/index.js index f538cbf..ab60043 100644 --- a/app/components/Article/index.js +++ b/app/components/Article/index.js @@ -1,7 +1,8 @@ import React from 'react'; -import { addHighlight } from 'actions/article'; +import { addHighlight, deleteHighlight, selectHighlight } from 'actions/article'; import { connect } from 'react-redux'; -import 'text-highlighter/src/TextHighlighter' +import jquery from 'jquery'; +//import 'text-highlighter/src/TextHighlighter' import { styles } from './styles.scss'; @@ -9,13 +10,20 @@ const mapDispatchToProps = dispatch => { return { onHighlight: (start, end, selectedText) => { dispatch(addHighlight(start, end, selectedText)); + }, + onDeleteHighlight: (source) => { + dispatch(deleteHighlight(source)); + }, + onSelectHighlight: (source) => { + dispatch(selectHighlight(source)); } }; } const mapStateToProps = state => { return { highlights: state.article.highlights, - currentTopic: state.article.currentTopic }; + currentTopic: state.article.currentTopic, + selectedHighlight: state.article.selectedHighlight,}; } const Article = React.createClass({ @@ -25,10 +33,187 @@ const Article = React.createClass({ propTypes: { article: React.PropTypes.object.isRequired, onHighlight: React.PropTypes.func, + onDeleteHighlight: React.PropTypes.func, + onSelectHighlight: React.PropTypes.func, highlights: React.PropTypes.array, - currentTopic: React.PropTypes.string + currentTopic: React.PropTypes.string, + selectedHighlight: React.PropTypes.array }, + /* + Domain: current stored highlight objects + Range: highlight-like objects that describe each text span + + 1. Takes the current highlights and breaks each into a start and end object, + 2. Sorts the objects by their index in the text, + 3. Creates a new highlight-like object for each segment between objects. These + objects will describe the spans that the render function creates. Each will have + its own combination of topics according to its overlap, + 4. Checks if span has been selected, if so changes selected property to True + 5. Activates or deactivates topics based on whether the object describes the + start of a highlight or the end of one + 6. Activates or deactivates source highlights (the highlights the span is representing) + 7. returns a list of span-objects with the same properties as highlight, which is passed + into render. + + No alterations were made to render or to the article reducer - all + this method does is reinterpret stored highlights so that render returns + distinct spans that appear to be overlapping + */ + processHighlights: function(highlights) { + var parsedHighlights = []; + var final = []; + + // (1) works + var temp_index = 0; + while (temp_index < highlights.length) { + var i = highlights[temp_index]; + var start = {type: 'start', index: i.start, topic: i.topic, source: i, selected: false}; + var end = {type: 'end', index: i.end, topic: i.topic, source: i, selected: false}; + parsedHighlights.push(start); + parsedHighlights.push(end); + temp_index += 1; + } + + // (2) works + parsedHighlights.sort((a,b) => { + return a.index - b.index; + }); + + var activeSources = []; + var activeTopics = [false, false, false, false]; + var topic_list = ['topic1', 'topic2', 'topic3', 'topic4']; + var activeSelect = false; + var start = 0; + var end = 0; + temp_index = 0; + + // (3) + var selectedHighlights = this.props.selectedHighlight; + while (temp_index < parsedHighlights.length) { + var i = parsedHighlights[temp_index]; + var processed = {start: null, end: null, topics: [], source: activeSources.slice(0), selected: false}; + processed.start = start; + processed.end = i.index; + + // (4) + if (selectedHighlights.length) { + var select_index = 0; + while (select_index < selectedHighlights.length) { + + var selected_high = selectedHighlights[select_index] + //Case for Single Highlight + if ((selected_high[0] == processed.start) && (selected_high[1] == processed.end)) { + processed.selected = true; + break; + } else if ((selected_high[0] < processed.start) && (processed.start < selected_high[1])) { + processed.selected = true; + break; + } else if ((selected_high[0] < processed.end) && (processed.end < selected_high[1])) { + processed.selected = true; + break; + } + select_index += 1; + } + } + + // Add processed span to final + start = i.index; + var list_index = 0; + while (list_index < activeTopics.length) { + if (activeTopics[list_index]) { + processed.topics.push(topic_list[list_index]); + } + list_index += 1; + } + final = final.concat(processed); + + // (5) Activate/Deactivate Topics + var active_state = i.type === 'start' + if (i.topic === '1') { + activeTopics[0] = active_state; + } else if (i.topic === '2') { + activeTopics[1] = active_state; + } else if (i.topic === '3') { + activeTopics[2] = active_state; + } else if (i.topic === '4') { + activeTopics[3] = active_state; + } + + // (6) Activate/Deactivate Sources + if (active_state){ + var active = {start: i.source.start, end: i.source.end, text: i.source.text, top: i.source.topic}; + activeSources = activeSources.concat([active]); + } else { + var active = {start: i.source.start, end: i.source.end, text: i.source.text, top: i.source.topic}; + var source_index = -1; + var index = 0; + if (activeSources){ + while (index < activeSources.length) { + var s = activeSources[index]; + if (s.start == active.start && s.end == active.end) { + source_index = index; + break; + } + index += 1; + } + } + activeSources.splice(source_index, 1); + } + temp_index += 1; + } + return final; + }, + /* + Domain: List of Topics + Range: String RGB + + From list of topics, gathers + */ + /* Need to deal with selected highlight */ + mergeColors: function(topics, selected) { + var list = []; + var index = 0; + while (index < topics.length) { + switch (topics[index]) { + case ('topic1'): + list.push('rgb(241, 96, 97)'); + break; + case ('topic2'): + list.push('rgb(253, 212, 132)'); + break; + case ('topic3'): + list.push('rgb(175, 215, 146)'); + break; + case ('topic4'): + list.push('rgb(168, 210, 191)'); + break; + } + index = index + 1; + } + var fraction = 1 / list.length; + var red = 0; + var blue = 0; + var green = 0; + index = 0; + while (index < list.length) { + var rgb = list[index].replace(/[^\d,]/g, '').split(','); + red += fraction * Number(rgb[0]); + green += fraction * Number(rgb[1]); + blue += fraction * Number(rgb[2]); + index += 1; + } + var opacity = 0.6; + if (selected) { + opacity = 1; + } + if (list.length == 0) { + return 'rgba(255, 255, 255, 0)'; + } + return 'rgba(' + Math.round(red) + ', ' + Math.round(green) + ', ' + Math.round(blue) + ', ' + opacity +')'; + }, + + componentDidMount: function() { let articleContainer = document.getElementById('article-container'); this.annotationsObject = new TextHighlighter(articleContainer); @@ -72,13 +257,39 @@ const Article = React.createClass({ } }, + componentDidMount: function() { + // unsure if jquery is necessary to mount keypress handler + // but this is what I found and it seems to work + var $ = jquery; + $(document.body).on('keydown', this.handleKeyDown); + }, + + componentWillUnmount: function() { + var $ = jquery; + $(document.body).off('keydown', this.handleKeyDown); + }, + + handleKeyDown: function(e) { + if (e.keyCode == 8 || e.keyCode == 46) { + e.preventDefault(); + if (this.props.selectedHighlight) { + this.props.onDeleteHighlight(this.props.selectedHighlight); + } + } + }, + + handleSelect: function(source, e) { + this.props.onSelectHighlight(source) + }, + + render() { // console.log(this.props); // const {topic_id}: string = this.context.params // let topic = this.props.topics[topic_id]; var text = this.props.article.text; - var highlights = this.props.highlights || []; + var highlights = this.processHighlights(this.props.highlights) || []; var start = 0; var tail = ''; @@ -105,7 +316,10 @@ const Article = React.createClass({ // render highlight start = curHL.end; return ({text.substring(curHL.start, curHL.end)}); } })} diff --git a/app/reducers/article.js b/app/reducers/article.js index 35aad9b..3f9b1f0 100644 --- a/app/reducers/article.js +++ b/app/reducers/article.js @@ -21,6 +21,7 @@ const initialState = Object.assign({ article: [], topics: [], highlights: [], + selectedHighlight: [], curArticle: null }, getInitialState()); @@ -31,7 +32,7 @@ function mergeHighlights(list) { for (var i = 0; i < n;) { var newrange = Object.assign({}, list[i]); for (var j = i + 1; j < n; j++) { - if (list[i].end >= list[j].start) { + if ((list[i].end >= list[j].start) && (list[i].topic === list[j].topic)) { newrange.text += list[j].text.substring( Math.min(newrange.end, list[j].end) - list[j].start, list[j].end ); @@ -78,6 +79,44 @@ export function article(state = initialState, action) { // curArticle: state.curArticle + 1 }); case 'ACTIVATE_TOPIC': return Object.assign({}, state, { currentTopic: action.topic }); + case 'SELECT_HIGHLIGHT': + /*Add start-end indices of clicked span to selectedHighlights + The indices are used in render to 'select' and darken the span*/ + var select = action.highlights; + var indices = []; + var i = 0; + while (i < select.length) { + var start = select[i].start; + var end = select[i].end; + indices.push([start, end]) + i += 1; + } + return Object.assign({}, state, { selectedHighlight: indices }); + case 'DELETE_HIGHLIGHT': + /*Remove selected highlights in state.highlights using the + indices from selectedHighlights. Also reset selectedHighlights*/ + var new_state = []; + var indices = []; + var stateindex = 0; + while (stateindex < state.highlights.length) { + var actionindex = 0; + var pushbool = true; + while (actionindex < action.highlights.length) { + var a_h = action.highlights[actionindex]; + // Highlight to be deleted + var s_h = state.highlights[stateindex]; + //Current highlights + if (a_h[0] == s_h.start && a_h[1] == s_h.end){ + pushbool = false; + } + actionindex += 1; + } + if (pushbool) { + new_state.push(s_h); + } + stateindex += 1; + } + return Object.assign({}, state, { highlights: new_state, selectedHighlight: []}); case 'GET_ARTICLE': return { ...state, From 4925175e27f090c6c93a57a56b4f4359e63dce88 Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Tue, 12 Jul 2016 12:15:51 -0700 Subject: [PATCH 04/10] pulled from merge-highlights and updated README and CONRTRIBUTING.md --- app/components/Article/index.js | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/app/components/Article/index.js b/app/components/Article/index.js index 87a0cb7..ab60043 100644 --- a/app/components/Article/index.js +++ b/app/components/Article/index.js @@ -39,6 +39,7 @@ const Article = React.createClass({ currentTopic: React.PropTypes.string, selectedHighlight: React.PropTypes.array }, + /* Domain: current stored highlight objects Range: highlight-like objects that describe each text span @@ -212,16 +213,10 @@ const Article = React.createClass({ return 'rgba(' + Math.round(red) + ', ' + Math.round(green) + ', ' + Math.round(blue) + ', ' + opacity +')'; }, + componentDidMount: function() { let articleContainer = document.getElementById('article-container'); - - var $ = jquery; - $(document.body).on('keydown', this.handleKeyDown); - }, - - componentWillUnmount: function() { - var $ = jquery; - $(document.body).off('keydown', this.handleKeyDown); + this.annotationsObject = new TextHighlighter(articleContainer); }, getOffset: function(childNodes, targetNode) { @@ -256,7 +251,6 @@ const Article = React.createClass({ start = end; end = tmp; } - console.log(start, end, selectedText); if (start !== end) { this.props.onHighlight(start, end, selectedText); } @@ -288,6 +282,7 @@ const Article = React.createClass({ this.props.onSelectHighlight(source) }, + render() { // console.log(this.props); // const {topic_id}: string = this.context.params @@ -319,17 +314,7 @@ const Article = React.createClass({ return ({text.substring(start, curHL.start)}); } else { // render highlight - // might want to create general method to check for object equality start = curHL.end; - var classStr = 'highlighted'; - if (this.state.selectedHighlight - && curHL.start === this.state.selectedHighlight.start - && curHL.end === this.state.selectedHighlight.end) { - classStr += ' selected' + this.state.selectedHighlight.topic; - } else { - classStr += ' topic' + curHL.topic; - } - return ( Date: Sun, 17 Jul 2016 22:58:52 -0700 Subject: [PATCH 05/10] added frame to add backend information --- CONTRIBUTING.md | 87 +++++++++++++++++++++++-------------------------- README.md | 37 ++++++++++++++++++--- 2 files changed, 73 insertions(+), 51 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b49ba05..711eb38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,84 +1,79 @@ -How the code in this repo works -=== +#How the code in this repo works + Hi! Glad you want to add code to this project. First, a brief overview of what's gone into this repo and some suggestions on how to get started using it. -The frontend stack ---- -* Redux -* React -* ES6 -* Sass -* Webpack +Formally, this repository contains a Redux app written with ES6 JavaScript, styled with Sass, and served with hotloading and module support from Webpack. It relies on a backend with a RESTful API, which is a Django server running PostgreSQL in Docker. -The backend stack ---- -* [Place information about backend here when merged] +The section "Uh, where does the code even start?" gives a brief overview. The two parts after that detail the frontend and backend stacks, respectively. -Formally, this repo contains a Redux app written with ES6 javascript and styled using Sass, served with hotloading and module support from webpack. It relies on a backend with a RESTful API found [here](https://github.com/Goodly/text-thresher-backend) which is a Django server running PostgreSQL in Docker. +#Uh, where does the code even start? -Uh, where does the code even start? ---- -The main body of code is in the `app` folder. In the `app` folder, `index.js` adds `Router`, the root React component. `Router` uses `routes.js` to decide what component to render for a given route/url. On the initial load, looking at `routes.js` shows that the `App` component from `app.js` is loaded for the route `/`, or at `http://localhost:3001/app/#/`. +The main body of code for the frontend is in the `app` folder. In the `app` folder, `index.js` adds `Router`, the root React component. `Router` uses `routes.js` to decide what component to render for a given route/url. On the initial load, looking at `routes.js` shows that the `App` component from `app.js` is loaded for the route `/`, or at `http://localhost:3001/app/#/`. -The part of the url after the `#` becomes the route fed into `routes.js`. The route for each `Route` tag is decided by the `path` property. The default url `http://localhost:3001/app/#/topics/0` means that the `App` and `TopicHighlighter` component are both rendered, with `TopicHighlighter` as the child of `App` and the `0` in the url the `:articleId` parameter for that `Route`. Thus, `/` only renders `App`, while going to `/topics/0` then renders `TopicHighlighter` as the child of the `App` component. The same idea can be applied to the `Quiz` component at the path `/quiz`. +The part of the url after the `#` becomes the route fed into `routes.js`. The route for each `Route` tag is decided by the `path` property. The default url `http://localhost:3001/app/#/topics/0` means that the `App` and `TopicHighlighter` component are both rendered, with `TopicHighlighter` as the child component of `App` and the `0` in the url the `:articleId` parameter for that `Route`. Thus, `/` only renders `App`, while going to `/topics/0` then renders `TopicHighlighter` as the child of the `App` component. The same idea can be applied to the `Quiz` component at the path `/quiz`. When `App` from `app.js` was loaded in `index.js`, it called `configureStore` from `appStore.js`, which set up Redux. This call in turn initializes the reducers, which perform the proper (synchronous for now) API calls from `api.js` to the back end so there's data (currently the news articles) to display. -The directories in `app` are described briefly below (most are self-explantory titles, but more information - if needed - is included here): +The directories in `app` are fairly self-explanatory. `actions` contains the action types and action creators, `components` the React components, `reducers` the Redux reducers, etc. -* `actions`: contains different action types and action creators. -* `assets`: contains mock-up `json` data of articles and quiz questions. This will eventually be replaced with data fetched from the backend. -* `components`: contains components, with those for the annotation view in `components/Article` and those for the quiz view in `components/quiz`. -* `container`: contains containers, which subscribe to Redux state. `app.js` in `containers/App` is the file mentioned above that initially sets up Redux. -* `reducers`: contains the reducers. -* `store`: contains `appStore.js`, the file mentioend above that is important in initially setting up Redux. -* `style`: various Sass files. -* `utils`: general helper files. +The backend code is briefly summarized below: + +**[overview of how the backend code functions]**. + +#The frontend stack + +* Redux +* React +* ES6 +* Sass +* Webpack -[Although not entirely important, I noticed project structure seems to have scss files in the style and not in the style folder. I also thought containers are connected to Redux state via the connect method, but most classes in components are also connected to Redux state. Is that an issue with project structure or meant to be that way?] +###What's React? -What's React? ---- React is like an extension of JavaScript that allows you to write markup HTML inline with logic. Each part of a page should have its own React component, and a React component, once created, can be treated as an HTML tag. For example, once `SomeReactComponent` is created, it can be used as HTML tag ``. React allows you to focus on presentational code that determines what to render on the page, and you can treat the presentational code as funnels which accept data and produce the correct visual change. The React docs aren't so great - more information on React is included in the guide linked to in the **Redux** section. -What's Redux? ---- +###What's Redux? + Redux is a framework exceptionally good for building understandable and manageable UIs, because of its unified state, unidirectional data flow, and pure functional mutations of state. The Redux docs ARE good, and you should read them until at least like the section labeled 'Advanced'. This one is the hardest to understand, after React. Read up and ask questions. -A brief overview of Redux workflow: when using Redux, there is a single state for the entire application. **Reducers** return certain parts of the state. When an event is triggered, a JavaScript object, called an **action**, representing the event, is sent to all the reducers. The reducers then, based on the action, make certain changes to Redux state, and a new Redux state is assembled. Now that we have a new, complete state, the React components are re-rendered accordingly. +A brief overview of the Redux flow: when using Redux, there is a single state for the entire application. **Reducers** return certain parts of the state. When an event is triggered, a JavaScript object, called an **action**, representing the event, is sent to all the reducers. The reducers then, based on the action's **type**, create a new Redux state (as Redux state is immutable), and a new Redux state is assembled. Now that we have a new, complete state, the React components are re-rendered accordingly. + +If you want more information, I wrote a hopefully comprehensive guide on React and Redux [here](https://gist.github.com/JasmineDeng/764dcd7be22288fadfe95bc83f051cd8). **[Feedback on the guide/if it's useful would be appreciated]** -If you want more information, I wrote a hopefully comprehensive guide on React and Redux [here](https://gist.github.com/JasmineDeng/764dcd7be22288fadfe95bc83f051cd8). [Feedback on the guide/if it's useful would be appreciated] +###What's ES6? -What's ES6? ---- ES6 is the next language release of JavaScript. It is made available now by the developer community of Babel, which also transpiles ES6 to ES5 (the current JavaScript release). Essentially, ES6 changed ES5 syntax to become prettier and more readable. Some major changes in ES6 involve: default values, arrow functions, template literals, the `let` and `const` keywords, etc. A full list of the new features in ES6 can be found [here](http://es6-features.org/). -What's Sass? --- +###What's Sass? + Syntactically Awesome StyleSheets - one of the leading preprocessors of CSS, it is a preprocessor that allows Sass code to be processed into ordinary CSS. You can do this most directly via `sass input.scss output.css`. Sass essentially changes the syntax of CSS to make it easier to use. The current Sass 3.0 is also called "sassy css". Variables, calculations, mixins, and nesting all help CSS scale way better. A quick guide to Sass can be found [here](http://sass-lang.com/guide). -What's Webpack? ---- +###What's Webpack? + Webpack is a module bundler. It takes in a bunch of files (HTML, JavaScript, CSS, SCSS, etc.) and turns it into something that can be provided to the client. The main benefits of Webpack are its powerful hot module reloading (instant updates to React components without refreshing), lazy loading (it only loads what you need), and the efficiency with which it detects, packages, and sends over changes and modules. -It produces similar results to Grunt and Gulp, but you don't need to write as much code. All the code for Webpack can be found in the `webpack` folder. [I don't really know how webpack works, so if anybody with a clearer idea wants to add info, go ahead.] +It produces similar results to Grunt and Gulp, but you don't need to write as much code. All the code for Webpack can be found in the `webpack` folder. **[I don't really know how webpack works, so might be unclear. Feed free to edit.]** + +#The backend stack + +* **[List the backend parts when merged]** + +###What's [backend]? **[Place backend information here when merged]** + +**[misc information]** -What's ___? [Place backend information here when merged] ---- -[misc information] +#What's ____? -What's ____? ---- I've tried to enumerate all the interesting and useful parts of all the above, so that you can Google the pieces easily. Developer support for this stuff is all great since it's pretty much cutting edge and widely accepted as the way to go. -The only thing we're not doing which would be great but not possible (Python has too many benefits for research) is isomorphic Redux, which just means the server also is in JavaScript and runs Redux. +The only thing we're not doing which would be great but not possible (Python has too many benefits for research) is isomorphic Redux, which just means the server also is in JavaScript and runs Redux. **[This problem may no longer be an issue?]** diff --git a/README.md b/README.md index 63e9f3a..7ff9515 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # annotator-content-analysis -An annotation interface for detailed text annotation by crowdworkers along researcher-defined topics of interest. + +An annotation interface for detailed text annotation by crowdworkers along researcher-defined topics of interest. Under development for the +[Deciding Force Project](http://www.decidingforce.org/). Currently, this app only runs locally. Built with [React](https://facebook.github.io/react/) and [Redux](https://github.com/reactjs/redux). @@ -12,15 +14,40 @@ npm install bower install ``` +The backend is supported by Docker. + +To install Docker: + * For Linux, go [here](https://docs.docker.com/engine/installation/). + * For OS X, go [here](https://docs.docker.com/engine/installation/mac/). + * For Windows, go [here](https://docs.docker.com/engine/installation/windows/). + +You might also want to install Devtools for [React](https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html). For Redux, you can install the [Google Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or use the other methods described in the README [here](https://github.com/zalmoxisus/redux-devtools-extension). + +####To run the Docker server + +Go to the project directory and follow the instructions below: -You might also want to install Devtools for [React](https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html). For Redux, you can install the [Google Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or use the other methods described in at the bottom of [this page](https://github.com/zalmoxisus/redux-devtools-extension) in the README. +1. Open up the Docker Quickstart Terminal and wait for the machine to start running. +2. On OSX, run `eval $(docker-machine env default)`. +3. Run `docker-compose up`. + +The server should now be up and running. Initialize it by running `./init_docker.sh`. **[Perhaps not necessary to run ./init_docker.sh?]** + +After this, you should be able to make your first query. + +**Note:** If you are setting up Docker a second time, you may want to first remove the database container with `docker-compose rm db`. ####To develop -Run `npm run dev` from the project dir to build and serve the development app. +In the project directory, run `npm run dev` and set up the Docker server as described above to build and serve the development app. + +To make your first query: + +- Find your Docker machine's ip by running `docker-machine ip default` in the project directory. Let's say it is `192.168.99.100`. +- Browse to `http://192.168.99.100:5000/api`. ####To deploy -Run `npm run deploy` and the output files will be written to the `dist` folder. +In the project dictory, run `npm run deploy` and set up the Docker server as described above. The output files will be written to the `dist` folder. -NOTE: this command currently is not fully functional and most likely will not work. Running `npm run dev` instead will show the most recent version of the code. \ No newline at end of file +**NOTE:** this command currently is not fully functional and most likely will not work. Running `npm run dev` instead will show the most recent version of the code. \ No newline at end of file From 4a028ab8a14512015af8be2f7f52052441017df6 Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Wed, 20 Jul 2016 18:36:44 -0700 Subject: [PATCH 06/10] only changed md files --- app/actions/article.js | 10 - app/components/Article/index.js | 226 +--------------------- app/components/Article/styles.scss | 15 -- app/containers/App/styles/_variables.scss | 2 +- app/reducers/article.js | 51 +---- 5 files changed, 13 insertions(+), 291 deletions(-) diff --git a/app/actions/article.js b/app/actions/article.js index fa7d1ec..d784d82 100644 --- a/app/actions/article.js +++ b/app/actions/article.js @@ -2,21 +2,11 @@ export const ADD_HIGHLIGHT = 'ADD_HIGHLIGHT'; export const ACTIVATE_TOPIC = 'ACTIVATE_TOPIC'; export const NEW_QUESTIONS = 'NEW_QUESTIONS'; export const NEW_ARTICLE = 'NEW_ARTICLE'; -export const DELETE_HIGHLIGHT = 'DELETE_HIGHLIGHT'; -export const SELECT_HIGHLIGHT = 'SELECT_HIGHLIGHT'; export function addHighlight(start, end, selectedText) { return { type: ADD_HIGHLIGHT, selection: {start, end, selectedText} }; } -export function deleteHighlight(source) { - return { type: DELETE_HIGHLIGHT, highlights: source }; -} - -export function selectHighlight(source) { - return { type: SELECT_HIGHLIGHT, highlights: source }; -} - export function activateTopic(topic) { return { type: ACTIVATE_TOPIC, topic }; } diff --git a/app/components/Article/index.js b/app/components/Article/index.js index ab60043..f538cbf 100644 --- a/app/components/Article/index.js +++ b/app/components/Article/index.js @@ -1,8 +1,7 @@ import React from 'react'; -import { addHighlight, deleteHighlight, selectHighlight } from 'actions/article'; +import { addHighlight } from 'actions/article'; import { connect } from 'react-redux'; -import jquery from 'jquery'; -//import 'text-highlighter/src/TextHighlighter' +import 'text-highlighter/src/TextHighlighter' import { styles } from './styles.scss'; @@ -10,20 +9,13 @@ const mapDispatchToProps = dispatch => { return { onHighlight: (start, end, selectedText) => { dispatch(addHighlight(start, end, selectedText)); - }, - onDeleteHighlight: (source) => { - dispatch(deleteHighlight(source)); - }, - onSelectHighlight: (source) => { - dispatch(selectHighlight(source)); } }; } const mapStateToProps = state => { return { highlights: state.article.highlights, - currentTopic: state.article.currentTopic, - selectedHighlight: state.article.selectedHighlight,}; + currentTopic: state.article.currentTopic }; } const Article = React.createClass({ @@ -33,187 +25,10 @@ const Article = React.createClass({ propTypes: { article: React.PropTypes.object.isRequired, onHighlight: React.PropTypes.func, - onDeleteHighlight: React.PropTypes.func, - onSelectHighlight: React.PropTypes.func, highlights: React.PropTypes.array, - currentTopic: React.PropTypes.string, - selectedHighlight: React.PropTypes.array + currentTopic: React.PropTypes.string }, - /* - Domain: current stored highlight objects - Range: highlight-like objects that describe each text span - - 1. Takes the current highlights and breaks each into a start and end object, - 2. Sorts the objects by their index in the text, - 3. Creates a new highlight-like object for each segment between objects. These - objects will describe the spans that the render function creates. Each will have - its own combination of topics according to its overlap, - 4. Checks if span has been selected, if so changes selected property to True - 5. Activates or deactivates topics based on whether the object describes the - start of a highlight or the end of one - 6. Activates or deactivates source highlights (the highlights the span is representing) - 7. returns a list of span-objects with the same properties as highlight, which is passed - into render. - - No alterations were made to render or to the article reducer - all - this method does is reinterpret stored highlights so that render returns - distinct spans that appear to be overlapping - */ - processHighlights: function(highlights) { - var parsedHighlights = []; - var final = []; - - // (1) works - var temp_index = 0; - while (temp_index < highlights.length) { - var i = highlights[temp_index]; - var start = {type: 'start', index: i.start, topic: i.topic, source: i, selected: false}; - var end = {type: 'end', index: i.end, topic: i.topic, source: i, selected: false}; - parsedHighlights.push(start); - parsedHighlights.push(end); - temp_index += 1; - } - - // (2) works - parsedHighlights.sort((a,b) => { - return a.index - b.index; - }); - - var activeSources = []; - var activeTopics = [false, false, false, false]; - var topic_list = ['topic1', 'topic2', 'topic3', 'topic4']; - var activeSelect = false; - var start = 0; - var end = 0; - temp_index = 0; - - // (3) - var selectedHighlights = this.props.selectedHighlight; - while (temp_index < parsedHighlights.length) { - var i = parsedHighlights[temp_index]; - var processed = {start: null, end: null, topics: [], source: activeSources.slice(0), selected: false}; - processed.start = start; - processed.end = i.index; - - // (4) - if (selectedHighlights.length) { - var select_index = 0; - while (select_index < selectedHighlights.length) { - - var selected_high = selectedHighlights[select_index] - //Case for Single Highlight - if ((selected_high[0] == processed.start) && (selected_high[1] == processed.end)) { - processed.selected = true; - break; - } else if ((selected_high[0] < processed.start) && (processed.start < selected_high[1])) { - processed.selected = true; - break; - } else if ((selected_high[0] < processed.end) && (processed.end < selected_high[1])) { - processed.selected = true; - break; - } - select_index += 1; - } - } - - // Add processed span to final - start = i.index; - var list_index = 0; - while (list_index < activeTopics.length) { - if (activeTopics[list_index]) { - processed.topics.push(topic_list[list_index]); - } - list_index += 1; - } - final = final.concat(processed); - - // (5) Activate/Deactivate Topics - var active_state = i.type === 'start' - if (i.topic === '1') { - activeTopics[0] = active_state; - } else if (i.topic === '2') { - activeTopics[1] = active_state; - } else if (i.topic === '3') { - activeTopics[2] = active_state; - } else if (i.topic === '4') { - activeTopics[3] = active_state; - } - - // (6) Activate/Deactivate Sources - if (active_state){ - var active = {start: i.source.start, end: i.source.end, text: i.source.text, top: i.source.topic}; - activeSources = activeSources.concat([active]); - } else { - var active = {start: i.source.start, end: i.source.end, text: i.source.text, top: i.source.topic}; - var source_index = -1; - var index = 0; - if (activeSources){ - while (index < activeSources.length) { - var s = activeSources[index]; - if (s.start == active.start && s.end == active.end) { - source_index = index; - break; - } - index += 1; - } - } - activeSources.splice(source_index, 1); - } - temp_index += 1; - } - return final; - }, - /* - Domain: List of Topics - Range: String RGB - - From list of topics, gathers - */ - /* Need to deal with selected highlight */ - mergeColors: function(topics, selected) { - var list = []; - var index = 0; - while (index < topics.length) { - switch (topics[index]) { - case ('topic1'): - list.push('rgb(241, 96, 97)'); - break; - case ('topic2'): - list.push('rgb(253, 212, 132)'); - break; - case ('topic3'): - list.push('rgb(175, 215, 146)'); - break; - case ('topic4'): - list.push('rgb(168, 210, 191)'); - break; - } - index = index + 1; - } - var fraction = 1 / list.length; - var red = 0; - var blue = 0; - var green = 0; - index = 0; - while (index < list.length) { - var rgb = list[index].replace(/[^\d,]/g, '').split(','); - red += fraction * Number(rgb[0]); - green += fraction * Number(rgb[1]); - blue += fraction * Number(rgb[2]); - index += 1; - } - var opacity = 0.6; - if (selected) { - opacity = 1; - } - if (list.length == 0) { - return 'rgba(255, 255, 255, 0)'; - } - return 'rgba(' + Math.round(red) + ', ' + Math.round(green) + ', ' + Math.round(blue) + ', ' + opacity +')'; - }, - - componentDidMount: function() { let articleContainer = document.getElementById('article-container'); this.annotationsObject = new TextHighlighter(articleContainer); @@ -257,39 +72,13 @@ const Article = React.createClass({ } }, - componentDidMount: function() { - // unsure if jquery is necessary to mount keypress handler - // but this is what I found and it seems to work - var $ = jquery; - $(document.body).on('keydown', this.handleKeyDown); - }, - - componentWillUnmount: function() { - var $ = jquery; - $(document.body).off('keydown', this.handleKeyDown); - }, - - handleKeyDown: function(e) { - if (e.keyCode == 8 || e.keyCode == 46) { - e.preventDefault(); - if (this.props.selectedHighlight) { - this.props.onDeleteHighlight(this.props.selectedHighlight); - } - } - }, - - handleSelect: function(source, e) { - this.props.onSelectHighlight(source) - }, - - render() { // console.log(this.props); // const {topic_id}: string = this.context.params // let topic = this.props.topics[topic_id]; var text = this.props.article.text; - var highlights = this.processHighlights(this.props.highlights) || []; + var highlights = this.props.highlights || []; var start = 0; var tail = ''; @@ -316,10 +105,7 @@ const Article = React.createClass({ // render highlight start = curHL.end; return ({text.substring(curHL.start, curHL.end)}); } })} diff --git a/app/components/Article/styles.scss b/app/components/Article/styles.scss index 2d81ade..89f5ce6 100644 --- a/app/components/Article/styles.scss +++ b/app/components/Article/styles.scss @@ -1,8 +1,6 @@ @import 'app/containers/App/styles/_mixins.scss'; @import 'app/containers/App/styles/_variables.scss'; -$darken-highlight: 10%; - .text-wrapper { max-width: 500px; margin: 0 auto; @@ -16,31 +14,18 @@ $darken-highlight: 10%; .highlighted { // TODO: generate programmatically? background-color: $red_accent; - &.topic1 { background-color: $red_accent; } - &.selected1 { - background-color: darken($red_accent, $darken-highlight); - } &.topic2 { background-color: $yellow_accent; } - &.selected2 { - background-color: darken($yellow_accent, $darken-highlight); - } &.topic3 { background-color: $green_accent; } - &.selected3 { - background-color: darken($green_accent, $darken-highlight); - } &.topic4 { background-color: $blue_accent; } - &.selected4 { - background-color: darken($blue_accent, $darken-highlight); - } } /* react makes this not work */ diff --git a/app/containers/App/styles/_variables.scss b/app/containers/App/styles/_variables.scss index 5ab6f01..a409045 100644 --- a/app/containers/App/styles/_variables.scss +++ b/app/containers/App/styles/_variables.scss @@ -12,4 +12,4 @@ $offwhite : #ededed; $red_accent : #F16061; $yellow_accent : #FDD484; $green_accent : #AFD792; -$blue_accent : #A8D2BF; +$blue_accent : #a8d2bf; diff --git a/app/reducers/article.js b/app/reducers/article.js index 5243f91..35aad9b 100644 --- a/app/reducers/article.js +++ b/app/reducers/article.js @@ -1,9 +1,8 @@ -import { ADD_HIGHLIGHT, - DELETE_HIGHLIGHT, - NEW_ARTICLE, - ACTIVATE_TOPIC } from '../actions/actionTypes'; +// import api from '../api.js'; + import articleJsonMock_0 from '../assets/article_0_mock.json'; import articleJsonMock_9 from '../assets/article_9_mock.json'; + import topicJsonMock from '../assets/topic_0_mock.json'; // NOTE: create mock article routes at `/topics/0` and `/topics/9` @@ -22,7 +21,6 @@ const initialState = Object.assign({ article: [], topics: [], highlights: [], - selectedHighlight: [], curArticle: null }, getInitialState()); @@ -33,9 +31,9 @@ function mergeHighlights(list) { for (var i = 0; i < n;) { var newrange = Object.assign({}, list[i]); for (var j = i + 1; j < n; j++) { - if ((list[i].end >= list[j].start) && (list[i].topic === list[j].topic)) { + if (list[i].end >= list[j].start) { newrange.text += list[j].text.substring( - Math.min(newrange.end, list[j].end) - list[j].start, list[j].text.length + Math.min(newrange.end, list[j].end) - list[j].start, list[j].end ); newrange.end = Math.max(list[j].end, newrange.end); continue; @@ -50,6 +48,7 @@ function mergeHighlights(list) { } export function article(state = initialState, action) { + console.log(action); switch (action.type) { case 'ADD_HIGHLIGHT': var newHighlights = state.highlights.concat( @@ -79,44 +78,6 @@ export function article(state = initialState, action) { // curArticle: state.curArticle + 1 }); case 'ACTIVATE_TOPIC': return Object.assign({}, state, { currentTopic: action.topic }); - case 'SELECT_HIGHLIGHT': - /*Add start-end indices of clicked span to selectedHighlights - The indices are used in render to 'select' and darken the span*/ - var select = action.highlights; - var indices = []; - var i = 0; - while (i < select.length) { - var start = select[i].start; - var end = select[i].end; - indices.push([start, end]) - i += 1; - } - return Object.assign({}, state, { selectedHighlight: indices }); - case 'DELETE_HIGHLIGHT': - /*Remove selected highlights in state.highlights using the - indices from selectedHighlights. Also reset selectedHighlights*/ - var new_state = []; - var indices = []; - var stateindex = 0; - while (stateindex < state.highlights.length) { - var actionindex = 0; - var pushbool = true; - while (actionindex < action.highlights.length) { - var a_h = action.highlights[actionindex]; - // Highlight to be deleted - var s_h = state.highlights[stateindex]; - //Current highlights - if (a_h[0] == s_h.start && a_h[1] == s_h.end){ - pushbool = false; - } - actionindex += 1; - } - if (pushbool) { - new_state.push(s_h); - } - stateindex += 1; - } - return Object.assign({}, state, { highlights: new_state, selectedHighlight: []}); case 'GET_ARTICLE': return { ...state, From 1986e97d222efe4f9ea7e0434e227e1d7bbce91c Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Sat, 23 Jul 2016 13:10:01 -0700 Subject: [PATCH 07/10] updated --- CONTRIBUTING.md | 20 +++++++++++--------- README.md | 18 ++++++------------ app/actions/actionTypes.js | 5 ----- app/actions/actions.js | 24 ------------------------ package.json | 3 +-- 5 files changed, 18 insertions(+), 52 deletions(-) delete mode 100644 app/actions/actionTypes.js delete mode 100644 app/actions/actions.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 711eb38..60d850b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -#How the code in this repo works +#Overview Hi! Glad you want to add code to this project. First, a brief overview of what's gone into this repo and some suggestions on how to get started using it. @@ -8,14 +8,10 @@ The section "Uh, where does the code even start?" gives a brief overview. The tw #Uh, where does the code even start? -The main body of code for the frontend is in the `app` folder. In the `app` folder, `index.js` adds `Router`, the root React component. `Router` uses `routes.js` to decide what component to render for a given route/url. On the initial load, looking at `routes.js` shows that the `App` component from `app.js` is loaded for the route `/`, or at `http://localhost:3001/app/#/`. - -The part of the url after the `#` becomes the route fed into `routes.js`. The route for each `Route` tag is decided by the `path` property. The default url `http://localhost:3001/app/#/topics/0` means that the `App` and `TopicHighlighter` component are both rendered, with `TopicHighlighter` as the child component of `App` and the `0` in the url the `:articleId` parameter for that `Route`. Thus, `/` only renders `App`, while going to `/topics/0` then renders `TopicHighlighter` as the child of the `App` component. The same idea can be applied to the `Quiz` component at the path `/quiz`. +The main body of code for the frontend is in the `app` folder. Upon first loading, `index.js` adds a `Router` component, which uses `routes.js` to decide what React component to render at its child at specific routes/urls. At `http://localhost:3001/app/#/topics/0`, React renders `TopicHighlighter` as the child of the `App` component. When `App` from `app.js` was loaded in `index.js`, it called `configureStore` from `appStore.js`, which set up Redux. This call in turn initializes the reducers, which perform the proper (synchronous for now) API calls from `api.js` to the back end so there's data (currently the news articles) to display. -The directories in `app` are fairly self-explanatory. `actions` contains the action types and action creators, `components` the React components, `reducers` the Redux reducers, etc. - The backend code is briefly summarized below: **[overview of how the backend code functions]**. @@ -42,7 +38,7 @@ Redux is a framework exceptionally good for building understandable and manageab A brief overview of the Redux flow: when using Redux, there is a single state for the entire application. **Reducers** return certain parts of the state. When an event is triggered, a JavaScript object, called an **action**, representing the event, is sent to all the reducers. The reducers then, based on the action's **type**, create a new Redux state (as Redux state is immutable), and a new Redux state is assembled. Now that we have a new, complete state, the React components are re-rendered accordingly. -If you want more information, I wrote a hopefully comprehensive guide on React and Redux [here](https://gist.github.com/JasmineDeng/764dcd7be22288fadfe95bc83f051cd8). **[Feedback on the guide/if it's useful would be appreciated]** +If you want more information, I wrote a hopefully comprehensive guide on React and Redux [here](https://gist.github.com/JasmineDeng/764dcd7be22288fadfe95bc83f051cd8). ###What's ES6? @@ -62,7 +58,7 @@ Webpack is a module bundler. It takes in a bunch of files (HTML, JavaScript, CSS The main benefits of Webpack are its powerful hot module reloading (instant updates to React components without refreshing), lazy loading (it only loads what you need), and the efficiency with which it detects, packages, and sends over changes and modules. -It produces similar results to Grunt and Gulp, but you don't need to write as much code. All the code for Webpack can be found in the `webpack` folder. **[I don't really know how webpack works, so might be unclear. Feed free to edit.]** +It produces similar results to Grunt and Gulp, but you don't need to write as much code. All the code for Webpack can be found in the `webpack` folder. #The backend stack @@ -76,4 +72,10 @@ It produces similar results to Grunt and Gulp, but you don't need to write as mu I've tried to enumerate all the interesting and useful parts of all the above, so that you can Google the pieces easily. Developer support for this stuff is all great since it's pretty much cutting edge and widely accepted as the way to go. -The only thing we're not doing which would be great but not possible (Python has too many benefits for research) is isomorphic Redux, which just means the server also is in JavaScript and runs Redux. **[This problem may no longer be an issue?]** +The only thing we're not doing which would be great but not possible (Python has too many benefits for research) is isomorphic Redux, which just means the server also is in JavaScript and runs Redux. + +#Additional information + +###React Routing + +The part of the url after the `#` becomes the route fed into `routes.js`. The route for each `Route` tag is decided by the `path` property. The default url `http://localhost:3001/app/#/topics/0` means that the `App` and `TopicHighlighter` component are both rendered, with `TopicHighlighter` as the child component of `App` and the `0` in the url the `:articleId` parameter for that `Route`. Thus, `/` only renders `App`, while going to `/topics/0` then renders `TopicHighlighter` as the child of the `App` component. The same idea can be applied to the `Quiz` component at the path `/quiz`. diff --git a/README.md b/README.md index 7ff9515..1e26d94 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,12 @@ You might also want to install Devtools for [React](https://facebook.github.io/r ####To run the Docker server -Go to the project directory and follow the instructions below: +Go to the project directory: -1. Open up the Docker Quickstart Terminal and wait for the machine to start running. -2. On OSX, run `eval $(docker-machine env default)`. -3. Run `docker-compose up`. +1. Run `docker-compose up`. +2. Run `./init_docker.sh`. -The server should now be up and running. Initialize it by running `./init_docker.sh`. **[Perhaps not necessary to run ./init_docker.sh?]** - -After this, you should be able to make your first query. +Once the containers are seeded, run `docker-compose start`. After this, you should be able to make your first query. **Note:** If you are setting up Docker a second time, you may want to first remove the database container with `docker-compose rm db`. @@ -41,13 +38,10 @@ After this, you should be able to make your first query. In the project directory, run `npm run dev` and set up the Docker server as described above to build and serve the development app. -To make your first query: - -- Find your Docker machine's ip by running `docker-machine ip default` in the project directory. Let's say it is `192.168.99.100`. -- Browse to `http://192.168.99.100:5000/api`. +To make your first query, navigate to `localhost:5000/api`. ####To deploy In the project dictory, run `npm run deploy` and set up the Docker server as described above. The output files will be written to the `dist` folder. -**NOTE:** this command currently is not fully functional and most likely will not work. Running `npm run dev` instead will show the most recent version of the code. \ No newline at end of file +**NOTE:** this command currently currently not fully functional and needs to be upgraded. Running `npm run dev` instead will show the most recent version of the code. \ No newline at end of file diff --git a/app/actions/actionTypes.js b/app/actions/actionTypes.js deleted file mode 100644 index 6ed680b..0000000 --- a/app/actions/actionTypes.js +++ /dev/null @@ -1,5 +0,0 @@ -export const ADD_HIGHLIGHT = 'ADD_HIGHLIGHT'; -export const ACTIVATE_TOPIC = 'ACTIVATE_TOPIC'; -export const NEW_ARTICLE = 'NEW_ARTICLE'; -export const NEW_QUESTIONS = 'NEW_QUESTIONS'; -export const DELETE_HIGHLIGHT = 'DELETE_HIGHLIGHT'; diff --git a/app/actions/actions.js b/app/actions/actions.js deleted file mode 100644 index e89246a..0000000 --- a/app/actions/actions.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as types from './actionTypes'; - -export function addHighlight(start, end, selectedText) { - return { type: types.ADD_HIGHLIGHT, selection: {start, end, selectedText} }; -} - -export function deleteHighlight(highlight) { - return { type: types.DELETE_HIGHLIGHT, highlight: highlight }; -} - -export function activateTopic(topic) { - return { type: types.ACTIVATE_TOPIC, topic }; -} - -export function newArticle(article) { - // Where article is the index of the next article to get, or null to indicate - // we should hit the backend for a fresh batch of articles - return { type: types.NEW_ARTICLE, article }; -} - -export function newQuestions(questions) { - return { type: types.NEW_QUESTIONS, questions }; -} - diff --git a/package.json b/package.json index e87ff4f..5a73362 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "dom-anchor-text-position": "^2.0.1", "immutable": "^3.7.6", "jquery": "^2.2.0", - "react-addons-css-transition-group": "^0.14.7", - "text-highlighter": "^1.0.2" + "react-addons-css-transition-group": "^0.14.7" } } From 93d2eb53cc0f63f58a0a098b84b0a47a215ea513 Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Wed, 3 Aug 2016 13:31:22 -0700 Subject: [PATCH 08/10] added backend information in README.md --- README.md | 52 +++++++++++++++++++++++++++++++++--------- app/actions/actions.js | 24 +++++++++++++++++++ 2 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 app/actions/actions.js diff --git a/README.md b/README.md index 1e26d94..27cd110 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# annotator-content-analysis +# TextThresher An annotation interface for detailed text annotation by crowdworkers along researcher-defined topics of interest. Under development for the [Deciding Force Project](http://www.decidingforce.org/). Currently, this app only runs locally. Built with [React](https://facebook.github.io/react/) and [Redux](https://github.com/reactjs/redux). +#Frontend + ####To setup From the project directory, run @@ -14,14 +16,30 @@ npm install bower install ``` +You might also want to install Devtools for [React](https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html). For Redux, you can install the [Google Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or use the other methods described in the README [here](https://github.com/zalmoxisus/redux-devtools-extension). + +####To develop + +In the project directory, run `npm run dev` and start the Docker server (described below) to build and serve the development app. + +####To deploy + +To deploy the fronted: + +In the project dictory, run `npm run deploy` and set up the Docker server as described above. The output files will be written to the `dist` folder. + +**NOTE:** this command currently currently not fully functional and needs to be upgraded. Running `npm run dev` instead will show the most recent version of the code. + +#Backend + +####To setup + The backend is supported by Docker. To install Docker: - * For Linux, go [here](https://docs.docker.com/engine/installation/). - * For OS X, go [here](https://docs.docker.com/engine/installation/mac/). - * For Windows, go [here](https://docs.docker.com/engine/installation/windows/). - -You might also want to install Devtools for [React](https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html). For Redux, you can install the [Google Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or use the other methods described in the README [here](https://github.com/zalmoxisus/redux-devtools-extension). +* For OS X, go [here](https://docs.docker.com/docker-for-mac/). +* For Windows, go [here](https://docs.docker.com/docker-for-windows/). +* For Ubuntu and other Linux distributions, go [here](https://docs.docker.com/engine/installation/linux/ubuntulinux/). ####To run the Docker server @@ -36,12 +54,24 @@ Once the containers are seeded, run `docker-compose start`. After this, you shou ####To develop -In the project directory, run `npm run dev` and set up the Docker server as described above to build and serve the development app. +Start the containers, which should already be seeded, with `docker-compose start`. -To make your first query, navigate to `localhost:5000/api`. +To view a browsable interface for the queries, navigate to `localhost:5000/api`. Another browsable interface is available [on Heroku](http://text-thresher.herokuapp.com/api/), but is not fully up-to-date. -####To deploy +####To Deploy -In the project dictory, run `npm run deploy` and set up the Docker server as described above. The output files will be written to the `dist` folder. +To deploy the backend to Heroku: + +- push the code to Heroku `git push heroku` + +- Reset the db with `heroku pg:reset postgres --confirm text-thresher` + +- Prepare the database. You have two options. + + - To initialize the database but not load data, run `heroku run python manage.py syncdb` + + - To initialize the database with a copy of your local data, verify that your +local postgres database has data and works when you run the app locally, +then run `heroku pg:push LOCAL_DB_NAME postgres` -**NOTE:** this command currently currently not fully functional and needs to be upgraded. Running `npm run dev` instead will show the most recent version of the code. \ No newline at end of file +- Visit the [application](http://text-thresher.herokuapp.com/api/) to make sure it worked. \ No newline at end of file diff --git a/app/actions/actions.js b/app/actions/actions.js new file mode 100644 index 0000000..e89246a --- /dev/null +++ b/app/actions/actions.js @@ -0,0 +1,24 @@ +import * as types from './actionTypes'; + +export function addHighlight(start, end, selectedText) { + return { type: types.ADD_HIGHLIGHT, selection: {start, end, selectedText} }; +} + +export function deleteHighlight(highlight) { + return { type: types.DELETE_HIGHLIGHT, highlight: highlight }; +} + +export function activateTopic(topic) { + return { type: types.ACTIVATE_TOPIC, topic }; +} + +export function newArticle(article) { + // Where article is the index of the next article to get, or null to indicate + // we should hit the backend for a fresh batch of articles + return { type: types.NEW_ARTICLE, article }; +} + +export function newQuestions(questions) { + return { type: types.NEW_QUESTIONS, questions }; +} + From 752859b58968a389a16ee6208f812ea2c9f2597e Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Wed, 3 Aug 2016 13:33:30 -0700 Subject: [PATCH 09/10] matched code with master --- app/actions/actions.js | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 app/actions/actions.js diff --git a/app/actions/actions.js b/app/actions/actions.js deleted file mode 100644 index e89246a..0000000 --- a/app/actions/actions.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as types from './actionTypes'; - -export function addHighlight(start, end, selectedText) { - return { type: types.ADD_HIGHLIGHT, selection: {start, end, selectedText} }; -} - -export function deleteHighlight(highlight) { - return { type: types.DELETE_HIGHLIGHT, highlight: highlight }; -} - -export function activateTopic(topic) { - return { type: types.ACTIVATE_TOPIC, topic }; -} - -export function newArticle(article) { - // Where article is the index of the next article to get, or null to indicate - // we should hit the backend for a fresh batch of articles - return { type: types.NEW_ARTICLE, article }; -} - -export function newQuestions(questions) { - return { type: types.NEW_QUESTIONS, questions }; -} - From 049dee58807bc6f7b7dabb6e666fa57d64d49e21 Mon Sep 17 00:00:00 2001 From: JasmineDeng Date: Sat, 27 Aug 2016 22:50:10 -0700 Subject: [PATCH 10/10] reorganized REAMDE --- README.md | 65 +++++++++++++++++++++---------------------------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 27cd110..6b2221e 100644 --- a/README.md +++ b/README.md @@ -5,60 +5,45 @@ An annotation interface for detailed text annotation by crowdworkers along resea Built with [React](https://facebook.github.io/react/) and [Redux](https://github.com/reactjs/redux). -#Frontend +# To setup -####To setup - -From the project directory, run - -``` -npm install -bower install -``` - -You might also want to install Devtools for [React](https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html). For Redux, you can install the [Google Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or use the other methods described in the README [here](https://github.com/zalmoxisus/redux-devtools-extension). - -####To develop - -In the project directory, run `npm run dev` and start the Docker server (described below) to build and serve the development app. - -####To deploy - -To deploy the fronted: - -In the project dictory, run `npm run deploy` and set up the Docker server as described above. The output files will be written to the `dist` folder. - -**NOTE:** this command currently currently not fully functional and needs to be upgraded. Running `npm run dev` instead will show the most recent version of the code. - -#Backend - -####To setup - -The backend is supported by Docker. - -To install Docker: +The backend is supported by Docker. If you do not have it already, you will need to install it. * For OS X, go [here](https://docs.docker.com/docker-for-mac/). * For Windows, go [here](https://docs.docker.com/docker-for-windows/). * For Ubuntu and other Linux distributions, go [here](https://docs.docker.com/engine/installation/linux/ubuntulinux/). -####To run the Docker server +Once installed, start the Docker application (if on a Mac), then go to the project directory and run: + +1. `docker-compose up` +2. `./init_docker.sh` +3. `npm install` +4. `bower install` -Go to the project directory: +You will only need to run the above commands once. Those will do the preliminary setup for the application by installing the dependencies and seeding the Docker containers to setup the database. -1. Run `docker-compose up`. -2. Run `./init_docker.sh`. +You might also want to install Devtools for [React](https://facebook.github.io/react/blog/2015/09/02/new-react-developer-tools.html). For Redux, you can install the [Google Chrome extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) or use the other methods described in the README [here](https://github.com/zalmoxisus/redux-devtools-extension). -Once the containers are seeded, run `docker-compose start`. After this, you should be able to make your first query. +**Note:** If you are setting up Docker a second time, you may want to first remove the previous database containers with `docker-compose rm db`. -**Note:** If you are setting up Docker a second time, you may want to first remove the database container with `docker-compose rm db`. +**Note:** If you are on Ubuntu and encounter the error: +``` +Couldn't connect to Docker daemon at http+unix://var/run/docker.sock - is it running? +``` +run `sudo groupadd docker` and `sudo usermod -aG docker $USER`. This is also listed as a first step [here](https://docs.docker.com/engine/installation/linux/ubuntulinux/#/create-a-docker-group) under "Optional Configs". -####To develop +# To develop -Start the containers, which should already be seeded, with `docker-compose start`. +In the project directory, run `docker-compose start` and `npm run dev`. To view a browsable interface for the queries, navigate to `localhost:5000/api`. Another browsable interface is available [on Heroku](http://text-thresher.herokuapp.com/api/), but is not fully up-to-date. -####To Deploy +**Note:** If you encounter an error that the module `text-highlighter/src/TextHighlighter` cannot be found, you will need to update brew by running `brew update`. + +# To deploy + +In the project dictory, run `docker-compose start` and `npm run deploy`. The output files will be written to the `dist` folder. + +**NOTE:** this command currently currently not fully functional and needs to be upgraded. Running `npm run dev` instead will show the most recent version of the code. To deploy the backend to Heroku: