diff --git a/.gitignore b/.gitignore index 4d29575..601ba11 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # production /build +# +*.swp + # misc .DS_Store .env.local diff --git a/README.md b/README.md index 9d9614c..acd25e6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +## Result Image + +![alt text](https://github.com/Jong25/swpp-unittest-tutorial/blob/master/result.png?raw=true) + ## Available Scripts In the project directory, you can run: diff --git a/backend/todo/migrations/0003_auto_20191009_1311.py b/backend/todo/migrations/0003_auto_20191009_1311.py new file mode 100644 index 0000000..a517013 --- /dev/null +++ b/backend/todo/migrations/0003_auto_20191009_1311.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.5 on 2019-10-09 13:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('todo', '0002_todo_done'), + ] + + operations = [ + migrations.AddField( + model_name='todo', + name='date', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='todo', + name='month', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='todo', + name='year', + field=models.IntegerField(default=2019), + ), + ] diff --git a/backend/todo/models.py b/backend/todo/models.py index 46fcb02..e999097 100644 --- a/backend/todo/models.py +++ b/backend/todo/models.py @@ -4,3 +4,6 @@ class Todo(models.Model): title = models.CharField(max_length=120) content = models.TextField() done = models.BooleanField(default=False) + year = models.IntegerField(default=2019) + month = models.IntegerField(default=1) + date = models.IntegerField(default=1) diff --git a/backend/todo/views.py b/backend/todo/views.py index 598af14..d40f339 100644 --- a/backend/todo/views.py +++ b/backend/todo/views.py @@ -20,6 +20,9 @@ def index(request, id=None): 'id': todo.id, 'title': todo.title, 'content': todo.content, + 'year': todo.year, + 'month': todo.month, + 'date': todo.date, 'done': todo.done, } return JsonResponse(response_dict, safe=False) @@ -30,14 +33,20 @@ def index(request, id=None): body = request.body.decode() title = json.loads(body)['title'] content = json.loads(body)['content'] + dueDate = json.loads(body)['dueDate'] except (KeyError, JSONDecodeError) as e: return HttpResponseBadRequest() - todo = Todo(title=title, content=content, done=False) + todo = Todo(title=title, content=content, + year=dueDate['year'], month=dueDate['month'], + date=dueDate['date'], done=False) todo.save() response_dict = { 'id': todo.id, 'title': todo.title, 'content': todo.content, + 'year': todo.year, + 'month': todo.month, + 'date': todo.date, 'done': todo.done, } return HttpResponse(json.dumps(response_dict), status=201) diff --git a/package.json b/package.json index 793dc98..aed177e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "react-router-dom": "^5.0.1", "react-scripts": "3.1.1", "redux": "^4.0.4", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "semantic-ui-react": "^0.88.1" }, "scripts": { "start": "react-scripts start", diff --git a/result.png b/result.png new file mode 100644 index 0000000..2338524 Binary files /dev/null and b/result.png differ diff --git a/src/App.js b/src/App.js index a6e850e..2b94fe0 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,7 @@ import React from 'react'; import './App.css'; import TodoList from './containers/TodoList/TodoList'; +import TodoCalendar from './containers/TodoCalendar/TodoCalendar'; import RealDetail from './containers/TodoList/RealDetail/RealDetail'; import NewTodo from './containers/TodoList/NewTodo/NewTodo'; @@ -14,12 +15,13 @@ function App(props) {
} /> +

Not Found

} />
-
+ ); } diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css new file mode 100644 index 0000000..efc1c9a --- /dev/null +++ b/src/components/Calendar/Calendar.css @@ -0,0 +1,20 @@ +.cell .date { + font-size: 20px; +} + +.todoTitle { + cursor: pointer; +} + +.notdone { + color: blue; +} + +.done { + text-decoration: line-through; + color: #adb5bd; +} + +.sunday { + color: red; +} diff --git a/src/components/Calendar/Calendar.js b/src/components/Calendar/Calendar.js new file mode 100644 index 0000000..a9a7c6d --- /dev/null +++ b/src/components/Calendar/Calendar.js @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import { Table } from 'semantic-ui-react' + +import './Calendar.css'; + +const CALENDAR_HEADER = ( + + + Sun + Mon + Tue + Wed + Thu + Fri + Sat + + +); + +const renderCalenderBody = (dates, todos, clickDone) => { + let i = 0; + const rows = []; + for (let week=0; week<5; week++){ + let day = 0; // Sunday + + let row = []; + for (let day=0; day<7; day++) { + const date = dates[i]; + if (date !== undefined && day === date.getDay()) { + row.push( + +
{date.getDate()}
+ { + todos.filter(todo => { + return todo.year === date.getFullYear() && + todo.month === date.getMonth() && + todo.date === date.getDate(); + }).map(todo => { + return ( +
clickDone(todo.id)}> + {todo.title} +
+ ) + }) + } +
+ ) + i++; + } else { + row.push( ) + } + } + rows.push(row); + } + + return ( + + {rows.map((row, i) => ({row}))} + + ); +} + +const renderCalendar = (dates, todos, clickDone) => ( + + {CALENDAR_HEADER} + {renderCalenderBody(dates, todos, clickDone)} +
+) + +const Calendar = (props) => { + const dates = []; + const year = props.year; + const month = props.month - 1; + let date = 1; + let maxDate = (new Date(year, month + 1, 0)).getDate(); + + for (let date=1; date<=maxDate; date++) { + dates.push(new Date(year, month, date)); + } + + return renderCalendar(dates, props.todos, props.clickDone); +} + +export default Calendar diff --git a/src/components/Calendar/Calendar.test.js b/src/components/Calendar/Calendar.test.js new file mode 100644 index 0000000..f64dd24 --- /dev/null +++ b/src/components/Calendar/Calendar.test.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import Calendar from './Calendar'; +import { getMockStore } from '../../test-utils/mocks'; +import * as actionCreators from '../../store/actions/todo'; + +const stubInitialState = { + todos: [ + {id: 1, title: 'TODO_TEST_TITLE_1', done: true, year: 2020, month: 1, date: 1}, + {id: 2, title: 'TODO_TEST_TITLE_2', done: false, year: 2020, month: 1, date: 31}, + {id: 3, title: 'TODO_TEST_TITLE_3', done: false, year: 2020, month: 1, date: 27}, + ], + selectedTodo: null, +}; + +const mockStore = getMockStore(stubInitialState); + +describe('', () => { + let calendar, spyToggleTodo, spyGetTodos; + + beforeEach(() => { + spyToggleTodo = jest.spyOn(actionCreators, 'toggleTodo') + .mockImplementation(id => { return dispatch => {}; }); + calendar = ( + + ); + spyGetTodos = jest.spyOn(actionCreators, 'getTodos') + .mockImplementation(() => { return dispatch => {}; }); + }) + + it('should render without errors', () => { + const component = shallow(); + const wrapper = component.find('.Calendar'); + }); + + it('should render Calendar', () => { + const component = mount(calendar); + let wrapper = component.find('.sun').at(0); + expect(wrapper.text()).toBe('Sun') + wrapper = component.find('.mon').at(0); + expect(wrapper.text()).toBe('Mon') + wrapper = component.find('.tue').at(0); + expect(wrapper.text()).toBe('Tue') + wrapper = component.find('.wed').at(0); + expect(wrapper.text()).toBe('Wed') + wrapper = component.find('.thu').at(0); + expect(wrapper.text()).toBe('Thu') + wrapper = component.find('.fri').at(0); + expect(wrapper.text()).toBe('Fri') + wrapper = component.find('.sat').at(0); + expect(wrapper.text()).toBe('Sat') + }); +}); diff --git a/src/components/Todo/Todo.css b/src/components/Todo/Todo.css index f2105b8..277d467 100644 --- a/src/components/Todo/Todo.css +++ b/src/components/Todo/Todo.css @@ -28,4 +28,10 @@ margin-left: 1rem; color: orange; font-weight: 800; -} \ No newline at end of file +} + +.Todo .due { + flex: 1; + text-align: left; + word-break: break-all; +} diff --git a/src/components/Todo/Todo.js b/src/components/Todo/Todo.js index 1755a8d..4f4105c 100644 --- a/src/components/Todo/Todo.js +++ b/src/components/Todo/Todo.js @@ -12,6 +12,7 @@ const Todo = (props) => { {props.title} {props.done &&
} +
due: {props.year}.{props.month+1}.{props.date}
diff --git a/src/containers/TodoCalendar/TodoCalendar.css b/src/containers/TodoCalendar/TodoCalendar.css new file mode 100644 index 0000000..ef70190 --- /dev/null +++ b/src/containers/TodoCalendar/TodoCalendar.css @@ -0,0 +1,10 @@ +.link{ + text-align: left; +} + +.header { + text-align: left; + font-size: 40px; + margin-left: 10rem; + font-weight: bold; +} diff --git a/src/containers/TodoCalendar/TodoCalendar.js b/src/containers/TodoCalendar/TodoCalendar.js new file mode 100644 index 0000000..91b24ca --- /dev/null +++ b/src/containers/TodoCalendar/TodoCalendar.js @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; + +import { NavLink } from 'react-router-dom'; + +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; +import Calendar from '../../components/Calendar/Calendar'; + +import * as actionCreators from '../../store/actions/index'; + +import './TodoCalendar.css'; + +class TodoCalendar extends Component { + state = { + year: 2019, + month: 10, + } + componentDidMount() { + this.props.onGetAll(); + } + + handleClickPrev = () => { + this.setState({ + year: this.state.month === 1 ? this.state.year - 1 : this.state.year, + month: this.state.month === 1 ? 12 : this.state.month - 1 + }) + } + + handleClickNext = () => { + this.setState({ + year: this.state.month === 12 ? this.state.year + 1 : this.state.year, + month: this.state.month === 12 ? 1 : this.state.month + 1 + }) + } + + render() { + return ( +
+
See TodoList
+
+ + {this.state.year}.{this.state.month} + +
+ +
+ ); + } +} + +const mapStateToProps = state => { + return { + storedTodos: state.td.todos, + }; +} + +const mapDispatchToProps = dispatch => { + return { + onToggleTodo: (id) => + dispatch(actionCreators.toggleTodo(id)), + onGetAll: () => + dispatch(actionCreators.getTodos()) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TodoCalendar)); diff --git a/src/containers/TodoCalendar/TodoCalendar.test.js b/src/containers/TodoCalendar/TodoCalendar.test.js new file mode 100644 index 0000000..f50050c --- /dev/null +++ b/src/containers/TodoCalendar/TodoCalendar.test.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { Provider } from 'react-redux'; +import { connectRouter, ConnectedRouter } from 'connected-react-router'; +import { Route, Redirect, Switch } from 'react-router-dom'; + +import TodoCalendar from './TodoCalendar'; +import { getMockStore } from '../../test-utils/mocks'; +import { history } from '../../store/store'; +import * as actionCreators from '../../store/actions/todo'; + +const stubInitialState = { + todos: [ + {id: 1, title: 'TODO_TEST_TITLE_1', done: true, year: 2020, month: 1, date: 1}, + {id: 2, title: 'TODO_TEST_TITLE_2', done: false, year: 2020, month: 1, date: 31}, + {id: 3, title: 'TODO_TEST_TITLE_3', done: false, year: 2020, month: 1, date: 27}, + ], + selectedTodo: null, +}; + +const mockStore = getMockStore(stubInitialState); + +describe('', () => { + let todoCalendar, spyGetTodos; + + beforeEach(() => { + todoCalendar = ( + + + + + + + + ); + spyGetTodos = jest.spyOn(actionCreators, 'getTodos') + .mockImplementation(() => { return dispatch => {}; }); + }) + + it('should render Calendar', () => { + const component = mount(todoCalendar); + expect(spyGetTodos).toBeCalledTimes(1); + }); + + it(`should decrement month on 'handleClickPrev'`, () => { + const component = mount(todoCalendar); + const wrapper = component.find('button').at(0); + wrapper.simulate('click'); + expect(component.find('.header').text()).toBe(" prev month 2019.9 next month "); + }); + + it(`should change year and month on 'handleClickPrev'`, () => { + const component = mount(todoCalendar); + const wrapper = component.find('button').at(0); + for(let i=0; i<10; i++) wrapper.simulate('click'); + expect(component.find('.header').text()).toBe(" prev month 2018.12 next month "); + }); + + it(`should increment month on 'handleClickNext'`, () => { + const component = mount(todoCalendar); + const wrapper = component.find('button').at(1); + wrapper.simulate('click'); + expect(component.find('.header').text()).toBe(" prev month 2019.11 next month "); + }); + + it(`should change year and month on 'handleClickNext'`, () => { + const component = mount(todoCalendar); + const wrapper = component.find('button').at(1); + for(let i=0; i<3; i++) wrapper.simulate('click'); + expect(component.find('.header').text()).toBe(" prev month 2020.1 next month "); + }); +}); + diff --git a/src/containers/TodoList/NewTodo/NewTodo.js b/src/containers/TodoList/NewTodo/NewTodo.js index 536cd50..63eafe8 100644 --- a/src/containers/TodoList/NewTodo/NewTodo.js +++ b/src/containers/TodoList/NewTodo/NewTodo.js @@ -10,10 +10,27 @@ class NewTodo extends Component { state = { title: '', content: '', + dueDate: { + year: '', + month: '', + date: '', + }, + } + + componentDidMount() { + const now = new Date(); + this.setState({ + ...this.state, + dueDate: { + year: now.getFullYear(), + month: now.getMonth() + 1, + date: now.getDate(), + }, + }) } postTodoHandler = () => { - this.props.onStoreTodo(this.state.title, this.state.content); + this.props.onStoreTodo(this.state.title, this.state.content, this.state.dueDate); } render() { @@ -22,6 +39,7 @@ class NewTodo extends Component {

Add a New Todo!

this.setState({ title: event.target.value })} @@ -31,6 +49,31 @@ class NewTodo extends Component { onChange={(event) => this.setState({ content: event.target.value })} > + + year this.setState({ + dueDate: {...this.state.dueDate, year: event.target.value } + })} + > + month this.setState({ + dueDate: {...this.state.dueDate, month: event.target.value } + })} + > + date this.setState({ + dueDate: {...this.state.dueDate, date: event.target.value } + })} + > ); @@ -39,9 +82,9 @@ class NewTodo extends Component { const mapDispatchToProps = dispatch => { return { - onStoreTodo: (title, content) => - dispatch(actionCreators.postTodo({ title: title, content: content })), + onStoreTodo: (title, content, dueDate) => + dispatch(actionCreators.postTodo({ title: title, content: content, dueDate: dueDate})), } }; -export default connect(null, mapDispatchToProps)(NewTodo); \ No newline at end of file +export default connect(null, mapDispatchToProps)(NewTodo); diff --git a/src/containers/TodoList/NewTodo/NewTodo.test.js b/src/containers/TodoList/NewTodo/NewTodo.test.js index 5696dc7..0ee073c 100644 --- a/src/containers/TodoList/NewTodo/NewTodo.test.js +++ b/src/containers/TodoList/NewTodo/NewTodo.test.js @@ -53,9 +53,24 @@ describe('', () => { it(`should set state properly on title input`, () => { const title = 'TEST_TITLE' const component = mount(newTodo); - const wrapper = component.find('input'); + let wrapper = component.find('.title'); + wrapper.simulate('change', { target: { value: title } }); + let newTodoInstance = component.find(NewTodo.WrappedComponent).instance(); + expect(newTodoInstance.state.title).toEqual(title); + expect(newTodoInstance.state.content).toEqual(''); + + wrapper = component.find('.year'); + wrapper.simulate('change', { target: { value: title } }); + expect(newTodoInstance.state.title).toEqual(title); + expect(newTodoInstance.state.content).toEqual(''); + + wrapper = component.find('.month'); + wrapper.simulate('change', { target: { value: title } }); + expect(newTodoInstance.state.title).toEqual(title); + expect(newTodoInstance.state.content).toEqual(''); + + wrapper = component.find('.date'); wrapper.simulate('change', { target: { value: title } }); - const newTodoInstance = component.find(NewTodo.WrappedComponent).instance(); expect(newTodoInstance.state.title).toEqual(title); expect(newTodoInstance.state.content).toEqual(''); }); diff --git a/src/containers/TodoList/TodoList.js b/src/containers/TodoList/TodoList.js index e53fae3..8a5a2b2 100644 --- a/src/containers/TodoList/TodoList.js +++ b/src/containers/TodoList/TodoList.js @@ -27,6 +27,9 @@ class TodoList extends Component { key={td.id} title={td.title} done={td.done} + year={td.year} + month={td.month} + date={td.date} clickDetail={() => this.clickTodoHandler(td)} clickDone={() => this.props.onToggleTodo(td.id)} clickDelete={() => this.props.onDeleteTodo(td.id)} @@ -35,12 +38,15 @@ class TodoList extends Component { }); return ( -
-
- {this.props.title} +
+
See Calendar
+
+
+ {this.props.title} +
+ {todos} + New Todo
- {todos} - New Todo
) } @@ -63,4 +69,4 @@ const mapDispatchToProps = dispatch => { } } -export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TodoList)); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(withRouter(TodoList)); diff --git a/src/store/actions/todo.js b/src/store/actions/todo.js index 191fb88..9d32227 100644 --- a/src/store/actions/todo.js +++ b/src/store/actions/todo.js @@ -33,12 +33,24 @@ export const postTodo_ = (td) => { id: td.id, title: td.title, content: td.content, + dueDate: { + year: td.year, + month: td.month, + date: td.date, + } }; }; export const postTodo = (td) => { + const todo = { + ...td, + dueDate: { + ...td.dueDate, + month: td.dueDate.month - 1, + }, + } return (dispatch) => { - return axios.post('/api/todo/', td) + return axios.post('/api/todo/', todo) .then(res => { dispatch(postTodo_(res.data)); dispatch(push('/todos/')); diff --git a/src/store/actions/todo.test.js b/src/store/actions/todo.test.js index 390a917..5fbd022 100644 --- a/src/store/actions/todo.test.js +++ b/src/store/actions/todo.test.js @@ -7,7 +7,12 @@ import store from '../store'; const stubTodo = { id: 0, title: 'title 1', - content: 'content 1' + content: 'content 1', + dueDate: { + year: 2020, + month: 10, + date: 8, + } }; describe('ActionCreators', () => { @@ -97,7 +102,7 @@ describe('ActionCreators', () => { }); }) - store.dispatch(actionCreators.postTodo()).then(() => { + store.dispatch(actionCreators.postTodo(stubTodo)).then(() => { expect(spy).toHaveBeenCalledTimes(1); done(); }); diff --git a/src/store/reducers/todo.js b/src/store/reducers/todo.js index 9c048d5..243b060 100644 --- a/src/store/reducers/todo.js +++ b/src/store/reducers/todo.js @@ -13,6 +13,9 @@ const reducer = (state = initialState, action) => { id: action.id, title: action.title, content: action.content, + year: action.year, + month: action.month, + date: action.date, done: action.done, }; return { ...state, todos: state.todos.concat(newTodo) }; @@ -40,4 +43,4 @@ const reducer = (state = initialState, action) => { return state; }; -export default reducer; \ No newline at end of file +export default reducer;