├── api ├── models │ ├── __init__.py │ └── task.py ├── routes │ ├── __init__.py │ └── task.py └── __init__.py ├── .jshintrc ├── run.py ├── .gitignore ├── app ├── public │ ├── js │ │ ├── dispatcher │ │ │ └── AppDispatcher.js │ │ ├── contants │ │ │ └── TaskConstants.js │ │ ├── components │ │ │ ├── BaseComponent.js │ │ │ ├── Header.js │ │ │ ├── DraggedItem.js │ │ │ ├── Footer.js │ │ │ ├── NewTask.js │ │ │ ├── TodoApp.js │ │ │ ├── TaskListItem.js │ │ │ └── TaskList.js │ │ ├── app.js │ │ ├── utils │ │ │ └── TaskAPI.js │ │ ├── actions │ │ │ └── TaskActions.js │ │ └── stores │ │ │ └── TaskStore.js │ ├── images │ │ └── drag_icon.png │ └── css │ │ └── main.css ├── __mocks__ │ └── superagent.js ├── support │ └── preprocessor.js ├── views │ └── index.ejs ├── package.json ├── server.js └── __tests__ │ ├── footer-test.js │ ├── newTask-test.js │ ├── taskActions-test.js │ ├── taskStore-test.js │ ├── taskListItem-test.js │ └── taskList-test.js ├── requirements.txt └── README.md /api/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true 3 | } -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from api import app 2 | app.run(debug=True) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | node_modules 4 | 5 | bundle.js 6 | bundle.min.js 7 | -------------------------------------------------------------------------------- /app/public/js/dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | import {Dispatcher} from 'flux'; 2 | 3 | export default new Dispatcher(); -------------------------------------------------------------------------------- /app/__mocks__/superagent.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get: jest.genMockFunction(), 3 | post: jest.genMockFunction() 4 | }; -------------------------------------------------------------------------------- /app/public/images/drag_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilpath/flask-react-todo/HEAD/app/public/images/drag_icon.png -------------------------------------------------------------------------------- /app/support/preprocessor.js: -------------------------------------------------------------------------------- 1 | var ReactTools = require('react-tools'); 2 | 3 | module.exports = { 4 | process: function(src) { 5 | return ReactTools.transform(src); 6 | } 7 | }; -------------------------------------------------------------------------------- /api/models/task.py: -------------------------------------------------------------------------------- 1 | from api import db 2 | 3 | class Task(db.Document): 4 | 5 | description = db.StringField() 6 | order = db.IntField() 7 | done = db.BoolField(default=False) 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Cors==2.0.1 3 | Flask-MongoAlchemy==0.7.1 4 | itsdangerous==0.24 5 | Jinja2==2.7.3 6 | MarkupSafe==0.23 7 | MongoAlchemy==0.19 8 | pymongo==2.8.1 9 | six==1.9.0 10 | Werkzeug==0.10.4 11 | wheel==0.24.0 12 | -------------------------------------------------------------------------------- /app/public/js/contants/TaskConstants.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | SET_TODOS: null, 5 | CREATE_TODO: null, 6 | SAVE_TODO: null, 7 | SAVE_TODOS: null, 8 | ADD_TODO: null, 9 | REORDER: null, 10 | }); -------------------------------------------------------------------------------- /app/public/js/components/BaseComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class BaseComponent extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | _bind(methods) { 10 | methods.forEach( method => this[method] = this[method].bind(this) ); 11 | }; 12 | 13 | } -------------------------------------------------------------------------------- /app/public/js/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from './BaseComponent.js'; 3 | 4 | export default class Header extends BaseComponent { 5 | 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 |

Todos

14 |
15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/public/js/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TaskAPI from './utils/TaskAPI.js'; 3 | import TaskStore from './stores/TaskStore.js'; 4 | import TodoApp from './components/TodoApp.js'; 5 | 6 | window.onload = () => { 7 | let tasks = document.getElementById('state').innerHTML; 8 | 9 | React.render( 10 | , 11 | document.getElementById('app') 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /app/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Todo 4 | 5 | 6 | 7 | 8 | 9 |
<%-app-%>
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Flask-React-Todo 2 | 3 | ##Getting Started 4 | 5 | This project is divided into two parts. A python app, built using Flask and MongoDB, which contains the API. The second part is a node app that is serving the web app built in React. 6 | 7 | ### API 8 | To start the API server you'll need to install the required dependencies using ```pip install > requirements.txt``` by standing in the project root. 9 | 10 | After the dependencies have been installed. Run ```python run.py``` to start the server. It runs on port 5000 11 | 12 | ### Web App 13 | To start the Node App ```cd app/``` and run ```npm install``` to install the required dependences. 14 | 15 | After that can use ```npm start``` to start the server on port 8001 and start waching the repository for changes to the React App. You will also be able to run the tests by running ```npm test```. 16 | 17 | -------------------------------------------------------------------------------- /app/public/js/utils/TaskAPI.js: -------------------------------------------------------------------------------- 1 | import request from 'superagent'; 2 | import TaskActions from '../actions/TaskActions.js'; 3 | 4 | const BASE_API_URL = 'http://localhost:5000/api/tasks'; 5 | 6 | export default { 7 | 8 | fetchTasks(callback) { 9 | request 10 | .get(BASE_API_URL) 11 | .end((err, res) => { 12 | if(!err && callback) { 13 | callback(res.body); 14 | } 15 | }); 16 | }, 17 | 18 | updateTask(task, callback) { 19 | let url = `${BASE_API_URL}/${task._id}`; 20 | request 21 | .put(url) 22 | .send(task) 23 | .end((err, res) => { 24 | if(err) { 25 | console.log(err); 26 | } 27 | }); 28 | }, 29 | 30 | createTask(newTask, callback) { 31 | request 32 | .post(BASE_API_URL) 33 | .send(newTask) 34 | .end((err, res) => { 35 | if(callback) { 36 | callback(err, res); 37 | } 38 | }); 39 | } 40 | 41 | }; -------------------------------------------------------------------------------- /app/public/js/components/DraggedItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from './BaseComponent.js'; 3 | import classNames from 'classnames'; 4 | 5 | const ReactPropTypes = React.PropTypes; 6 | 7 | export default class DraggedItem extends BaseComponent { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render() { 14 | let classes = classNames({ 15 | 'todo-list__item': true, 16 | 'todo-list__item--dragging': true, 17 | 'todo-list__item--done': this.props.task.done 18 | }); 19 | 20 | let styles = { 21 | position: 'absolute', 22 | top: this.props.draggingInfo.top, 23 | left: this.props.draggingInfo.left 24 | }; 25 | 26 | return ( 27 |
  • 28 | 29 | 30 |
  • 31 | ); 32 | } 33 | } 34 | 35 | DraggedItem.PropTypes = { 36 | task: ReactPropTypes.object.isRequired 37 | }; -------------------------------------------------------------------------------- /app/public/js/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from './BaseComponent.js'; 3 | import TaskActions from '../actions/TaskActions.js'; 4 | 5 | const ReactPropTypes = React.PropTypes; 6 | 7 | export default class Footer extends BaseComponent { 8 | 9 | constructor(props) { 10 | super(props); 11 | this._bind(['_completeAll']); 12 | } 13 | 14 | render() { 15 | let count = this.props.remainingItems.length; 16 | let plural = count === 1 ? '' : 's'; 17 | let remainingText = `${count} item${plural} left`; 18 | 19 | return ( 20 | 26 | ); 27 | 28 | } 29 | 30 | _completeAll(event) { 31 | event.preventDefault(); 32 | TaskActions.completeAll(this.props.tasks); 33 | } 34 | 35 | } 36 | 37 | Footer.propTypes = { 38 | tasks: ReactPropTypes.array.isRequired, 39 | remainingItems: ReactPropTypes.array.isRequired 40 | }; -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, make_response 2 | from flask.ext.mongoalchemy import MongoAlchemy 3 | from flask.ext.cors import CORS 4 | import datetime 5 | import json 6 | from bson.objectid import ObjectId 7 | from werkzeug import Response 8 | 9 | app = Flask(__name__) 10 | app.config['MONGOALCHEMY_DATABASE'] = 'flask-react-todo' 11 | db = MongoAlchemy(app) 12 | cors = CORS(app) 13 | 14 | class MongoJsonEncoder(json.JSONEncoder): 15 | def default(self, obj): 16 | if isinstance(obj, (datetime.datetime, datetime.date)): 17 | return obj.isoformat() 18 | elif isinstance(obj, ObjectId): 19 | return str(obj) 20 | return json.JSONEncoder.default(self, obj) 21 | 22 | def jsonify(*args, **kwargs): 23 | return Response(json.dumps(*args, cls=MongoJsonEncoder), mimetype='application/json', **kwargs) 24 | 25 | 26 | @app.errorhandler(404) 27 | def not_found(error): 28 | return make_response(jsonify({'error': 'Not found'}), 404) 29 | 30 | @app.errorhandler(400) 31 | def bad_request(error): 32 | return make_response(jsonify({'error': 'Bad request'}), 400) 33 | 34 | @app.route('/') 35 | def index(): 36 | return "Hello, World!" 37 | 38 | from api.routes import task -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flask-react-todo-app", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "./node_modules/.bin/babel-node server.js", 8 | "build": "watchify -o public/js/bundle.js -v -d public/js/app.js", 9 | "test": "jest" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "babel": "^5.5.8", 15 | "body-parser": "^1.12.4", 16 | "classnames": "^2.1.2", 17 | "ejs": "^2.3.1", 18 | "express": "^4.12.4", 19 | "flux": "^2.0.3", 20 | "keymirror": "^0.1.1", 21 | "node-jsx": "^0.13.3", 22 | "object-assign": "^2.0.0", 23 | "path": "^0.11.14", 24 | "react": "^0.13.3", 25 | "superagent": "^1.2.0" 26 | }, 27 | "devDependencies": { 28 | "babel-jest": "^5.3.0", 29 | "babelify": "^6.1.2", 30 | "browserify": "^10.2.3", 31 | "jasmine-react": "^1.1.0", 32 | "jest-cli": "^0.4.7", 33 | "reactify": "^1.1.1", 34 | "uglify-js": "^2.4.23", 35 | "watchify": "^3.2.1" 36 | }, 37 | "browserify": { 38 | "transform": [ 39 | "babelify", 40 | "reactify" 41 | ] 42 | }, 43 | "jest": { 44 | "scriptPreprocessor": "/node_modules/babel-jest", 45 | "unmockedModulePathPatterns": [ 46 | "/node_modules/react" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/public/js/components/NewTask.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from './BaseComponent.js'; 3 | import TaskActions from '../actions/TaskActions.js'; 4 | 5 | export default class NewTask extends BaseComponent { 6 | 7 | constructor(props) { 8 | super(props); 9 | this._bind([ 10 | '_defaultState', 11 | '_handleChange', 12 | '_addTask' 13 | ]); 14 | 15 | this.state = this._defaultState(); 16 | } 17 | 18 | render() { 19 | let description = this.state.description; 20 | 21 | return ( 22 |
    23 | 31 | 32 |
    33 | ); 34 | } 35 | 36 | _defaultState() { 37 | return { 38 | description: '' 39 | }; 40 | } 41 | 42 | _handleChange(event) { 43 | this.setState({description: event.target.value}); 44 | } 45 | 46 | _addTask(event) { 47 | event.preventDefault(); 48 | if(!!this.state.description) { 49 | TaskActions.createTask({description: this.state.description}); 50 | this.setState(this._defaultState()); 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/public/js/components/TodoApp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from './BaseComponent.js'; 3 | import NewTask from './NewTask.js'; 4 | import TaskList from './TaskList.js'; 5 | import Footer from './Footer.js'; 6 | import Header from './Header.js'; 7 | import TaskStore from '../stores/TaskStore.js'; 8 | 9 | const ReactPropTypes = React.PropTypes; 10 | 11 | function getState() { 12 | return { 13 | tasks: TaskStore.getTasks(), 14 | remainingItems: TaskStore.remainingItems() 15 | }; 16 | } 17 | 18 | export default class TodoApp extends BaseComponent { 19 | 20 | constructor(props) { 21 | super(props); 22 | this._bind(['_onChange']); 23 | 24 | let tasks = this.props.tasks || []; 25 | TaskStore.setTasks(tasks); 26 | this.state = getState(); 27 | } 28 | 29 | componentDidMount() { 30 | TaskStore.addChangeListener(this._onChange); 31 | } 32 | 33 | componentWillUnmount() { 34 | TaskStore.removeChangeListener(this._onChange); 35 | } 36 | 37 | render() { 38 | return ( 39 |
    40 |
    41 | 42 | 43 |
    47 |
    48 | ); 49 | 50 | } 51 | 52 | _onChange() { 53 | this.setState(getState()); 54 | } 55 | 56 | } 57 | 58 | TodoApp.propTypes = { 59 | tasks: ReactPropTypes.array 60 | }; -------------------------------------------------------------------------------- /app/public/js/components/TaskListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from './BaseComponent.js'; 3 | import classNames from 'classnames'; 4 | import TaskActions from '../actions/TaskActions.js'; 5 | 6 | const ReactPropTypes = React.PropTypes; 7 | 8 | export default class TaskListItem extends BaseComponent { 9 | 10 | constructor(props) { 11 | super(props); 12 | this._bind(['_toggleDone']); 13 | } 14 | 15 | render() { 16 | let task = this.props.task; 17 | let classes = classNames({ 18 | 'todo-list__item': true, 19 | 'todo-list__item--done': task.done 20 | }); 21 | 22 | return ( 23 |
  • 31 | 38 | 39 |
  • 40 | ); 41 | 42 | } 43 | 44 | _toggleDone() { 45 | TaskActions.toggleDone(this.props.task); 46 | } 47 | 48 | } 49 | 50 | TaskListItem.propTypes = { 51 | task: ReactPropTypes.object.isRequired, 52 | order: ReactPropTypes.number.isRequired, 53 | onDragStart: ReactPropTypes.func.isRequired, 54 | onDragOver: ReactPropTypes.func.isRequired, 55 | onDragEnd: ReactPropTypes.func.isRequired 56 | }; -------------------------------------------------------------------------------- /app/public/js/actions/TaskActions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher/AppDispatcher.js'; 2 | import TaskConstants from '../contants/TaskConstants.js'; 3 | 4 | export default { 5 | 6 | setTasks(tasks) { 7 | AppDispatcher.dispatch({ 8 | actionType: TaskConstants.SET_TODOS, 9 | data: tasks 10 | }); 11 | }, 12 | 13 | saveTask(newTask) { 14 | AppDispatcher.dispatch({ 15 | actionType: TaskConstants.SAVE_TODO, 16 | data: newTask 17 | }); 18 | }, 19 | 20 | createTask(newTask) { 21 | AppDispatcher.dispatch({ 22 | actionType: TaskConstants.CREATE_TODO, 23 | data: newTask 24 | }); 25 | }, 26 | 27 | addTask(task) { 28 | AppDispatcher.dispatch({ 29 | actionType: TaskConstants.ADD_TODO, 30 | data: task 31 | }); 32 | }, 33 | 34 | reorder(task, from, to) { 35 | AppDispatcher.dispatch({ 36 | actionType: TaskConstants.REORDER, 37 | data: { task, from, to } 38 | }); 39 | }, 40 | 41 | saveTasks(tasks) { 42 | AppDispatcher.dispatch({ 43 | actionType: TaskConstants.SAVE_TODOS, 44 | data: tasks 45 | }); 46 | }, 47 | 48 | toggleDone(task) { 49 | task.done = !task.done; 50 | 51 | AppDispatcher.dispatch({ 52 | actionType: TaskConstants.SAVE_TODO, 53 | data: task 54 | }); 55 | }, 56 | 57 | completeAll(tasks) { 58 | 59 | let toUpdate = tasks 60 | .filter(task => !task.done) 61 | .map(task => { 62 | task.done = true; 63 | return task; 64 | }); 65 | 66 | AppDispatcher.dispatch({ 67 | actionType: TaskConstants.SAVE_TODOS, 68 | data: toUpdate 69 | }); 70 | } 71 | 72 | }; -------------------------------------------------------------------------------- /app/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import request from 'superagent'; 4 | import react from 'react'; 5 | import TaskAPI from './public/js/utils/TaskAPI.js'; 6 | import todoapp from './public/js/components/TodoApp.js'; 7 | import bodyParser from 'body-parser'; 8 | 9 | let port = 8001; 10 | let app = express(); 11 | let TodoApp = react.createFactory(todoapp); 12 | 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({ 15 | extended: true 16 | })); 17 | 18 | app.use(express.static(path.join(__dirname, 'public'))); 19 | app.set('views', path.join(__dirname, 'views')); 20 | app.set('view engine', 'ejs'); 21 | 22 | // routes 23 | app.get('/', (req, res) => { 24 | 25 | TaskAPI.fetchTasks(tasks => { 26 | let html = react.renderToString( 27 | TodoApp({ 28 | tasks: tasks 29 | }) 30 | ); 31 | 32 | res.render('index.ejs', { 33 | app: html, 34 | state: JSON.stringify(tasks) 35 | }); 36 | }); 37 | 38 | }); 39 | 40 | app.post('/tasks/new', (req, res) => { 41 | let description = req.body.description; 42 | 43 | if(description) { 44 | let task = {description}; 45 | TaskAPI.createTask(task, () => res.redirect('/')); 46 | } 47 | 48 | }); 49 | 50 | app.post('/tasks/:id/toggle', (req, res) => { 51 | let _id = req.params.id; 52 | let done = !!req.body.done; 53 | let order = Number(req.body.order); 54 | let description = req.body.description; 55 | let task = {_id, description, order, done}; 56 | 57 | if(_id) { 58 | TaskAPI.updateTask(task, () => res.redirect('/')); 59 | } 60 | }); 61 | 62 | app.post('/tasks/allDone', (req, res) => { 63 | 64 | TaskAPI.fetchTasks(tasks => { 65 | let undone = tasks.filter(task => !tasks.done ); 66 | 67 | undone.forEach(task => { 68 | task.done = true; 69 | TaskAPI.updateTask(task); 70 | }); 71 | 72 | res.redirect('/'); 73 | 74 | }); 75 | 76 | }); 77 | 78 | app.listen(port, () => console.log('listening on port: ', port)); -------------------------------------------------------------------------------- /app/__tests__/footer-test.js: -------------------------------------------------------------------------------- 1 | jest.dontMock('../public/js/components/BaseComponent.js'); 2 | jest.dontMock('../public/js/components/Footer.js'); 3 | jest.dontMock('../public/js/actions/TaskActions.js'); 4 | 5 | var React = require('react/addons'), 6 | Footer = require('../public/js/components/Footer.js'), 7 | TaskActions = require('../public/js/actions/TaskActions.js'), 8 | TestUtils = React.addons.TestUtils; 9 | 10 | describe('Footer: ', function () { 11 | 12 | var tasks; 13 | 14 | beforeEach(function () { 15 | 16 | tasks = [ 17 | {description: 'test task one', done: false}, 18 | {description: 'test task two', done: false}, 19 | {description: 'test task three', done: false} 20 | ]; 21 | 22 | }); 23 | 24 | function renderComponent() { 25 | var remaining = tasks.filter(function(task){ return !task.done; }); 26 | 27 | return TestUtils.renderIntoDocument( 28 |