From 9fe43828e2b5e5213fbd5bc0ad23a56d6def8a66 Mon Sep 17 00:00:00 2001 From: Nisha Date: Fri, 9 Oct 2020 14:02:49 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=93=96=20DOC:=20Update=20Readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index ef2666f..a78aaef 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,10 @@ sudo apt-get install libpq-dev python-dev sudo apt-get install postgresql postgresql-contrib ``` -### Install Postgresql **_Windows_** -[Tutorial PostgreSQL windows](https://www.postgresqltutorial.com/install-postgresql/) ### Setup Database and User in PostgreSQL -Based on these settings in [settings.py](/src/project/project/settings.py) ```python DATABASES = { From 734f02dfccfcec275459aa624fbdb3526f469155 Mon Sep 17 00:00:00 2001 From: Nisha Date: Fri, 9 Oct 2020 14:06:37 -0400 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=96=20DOC:=20Environment=20Install?= =?UTF-8?q?ation=20instructions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a78aaef..1171d69 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,31 @@ -# react-django-graphQL-postgres +# Coding Challenge GraphQL API in Django -- Nisha Chaube + - Nisha Chaube +Method 1: ## Setting up using Docker -Note: Below step may take some time +Note: Below steps may take some time -docker-compose up + docker-compose up -d -docker ps + docker ps - + -docker exec -it ####### /bin/bash + docker exec -it ####### /bin/bash -python manage.py makemigrations + python manage.py makemigrations -python manage.py migrate + python manage.py migrate -python manage.py loaddata ./Fixtures/initial_data.json - -Django server url: http://127.0.0.1:8000/graphql/ -React frontend url: http://localhost:3000/ + python manage.py loaddata ./Fixtures/initial_data.json + Django server url: http://127.0.0.1:8000/graphql/
+ React frontend url: http://localhost:3000/ + + +###################################################################### +Method 2: ## Setting up Python environment To get this project up and running you should start by having Python installed on your computer. It's advised you create a virtual environment to store your projects dependencies separately. You can install virtualenv with
@@ -87,3 +91,14 @@ DATABASES = { ```SQL grant all privileges on database postgres to postgres ``` + +## Setting up Python environment +
+ +``` +cd ui + +npm install + +npm start +``` From 89813ff384470dac22557856f5e243b254ad7da3 Mon Sep 17 00:00:00 2001 From: Nisha Date: Sun, 11 Oct 2020 14:06:24 -0400 Subject: [PATCH 3/3] Refractor the ui and backend code --- ui/package.json | 6 + ui/src/App.js | 2 +- ui/src/client.js | 39 +++++ ui/src/component/AccordionGroup.js | 138 +++++++++++++++ ui/src/component/City.js | 74 +++++++++ ui/src/component/Loading.js | 8 + ui/src/component/Modal.js | 258 +++++++++++++---------------- ui/src/component/WorldlMap.js | 2 +- ui/src/container/MapPage.js | 258 +++++------------------------ ui/src/container/Query.js | 43 +++++ ui/src/index.js | 12 +- worldapi/schema.py | 12 +- 12 files changed, 476 insertions(+), 376 deletions(-) create mode 100644 ui/src/client.js create mode 100644 ui/src/component/AccordionGroup.js create mode 100644 ui/src/component/City.js create mode 100644 ui/src/component/Loading.js create mode 100644 ui/src/container/Query.js diff --git a/ui/package.json b/ui/package.json index fab7672..218d831 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,9 +4,15 @@ "private": true, "dependencies": { "@apollo/client": "^3.2.2", + "@apollo/react-hooks": "^4.0.0", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", + "apollo": "^2.31.0", + "apollo-cache-inmemory": "^1.6.6", + "apollo-client": "^2.6.10", + "apollo-link-context": "^1.0.20", + "apollo-link-http": "^1.5.17", "bootstrap": "^4.5.2", "collections": "^5.1.12", "graphql": "^15.3.0", diff --git a/ui/src/App.js b/ui/src/App.js index f78f4be..ae6dfca 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -11,7 +11,7 @@ function App() { { return ( - + + new Promise((success, fail) => { + setTimeout(() => { + success() + }, 800) + }) +) +const link = ApolloLink.from([ + delay, + http +]) +const cache = new InMemoryCache() +const client = new ApolloClient({ + link, + cache, + onError: ({ graphQLErrors, networkError }) => { + if (graphQLErrors) { + graphQLErrors.map(({ message, locations, path }) => + console.log( + `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`), + console.log('This is a graphQL error!'), + ); + } + if (networkError) { + console.log('This is a Network Error!', networkError); + console.log('Can be called from a query error in the browser code!'); + } + } +}) + +export default client; \ No newline at end of file diff --git a/ui/src/component/AccordionGroup.js b/ui/src/component/AccordionGroup.js new file mode 100644 index 0000000..c3936e0 --- /dev/null +++ b/ui/src/component/AccordionGroup.js @@ -0,0 +1,138 @@ +import React, { useState, useEffect } from 'react' +import Map from 'collections/map' +import ListGroup from 'react-bootstrap/ListGroup' +import Accordion from 'react-bootstrap/Accordion' +import Card from 'react-bootstrap/Card' +import { GET_REGIONS } from '../container/Query' +import { useQuery } from '@apollo/react-hooks' +import City from './City' +import Loading from '../component/Loading' + +const AccordionGroup = ({ selectedContinent }) => { + const [selectedRegion, setSelectedRegion] = useState(null) + const [selectedCountry, setSelectedCountry] = useState(null) + const [selectedCountryCode, setSelectedCountryCode] = useState(null) + const [countryData, setCountryData] = useState([]) + + useEffect(() => { + setSelectedCountry(null) + setSelectedCountryCode(null) + setCountryData([]) + setSelectedRegion(null) + console.log('value changed selectedContinent!') + + }, [selectedContinent]); + + const { loading, error, data } = useQuery(GET_REGIONS, { + variables: { + "countryContinent": `${selectedContinent}` + } + }); + + if (loading) return + if (error) return

An error occured

+ + + /* ************************************************************ */ + /* Get countries on selected region + /* ************************************************************ */ + const handleRegionClick = (selectedRegion) => { + let countryData = data.getAllRegions.filter(item => { + return item.countryRegion === selectedRegion; + }); + setSelectedRegion(selectedRegion) + setCountryData(countryData) + setSelectedCountryCode(null) + setSelectedCountry(null) + } + + /* ************************************************************ */ + /* Get cities on selected country based on the country code + /* ************************************************************ */ + const handleCountryClick = (item) => { + let countryCode = item.countryCode, + countryName = item.countryName + + setSelectedCountry(countryName) + setSelectedCountryCode(countryCode) + } + + const getRegionList = (data) => { + const result = []; + const map = new Map(); + + for (const item of data) { + if (!map.has(item.countryRegion)) { + map.set(item.countryRegion, true); // set any value to Map + result.push({ countryRegion: item.countryRegion }); + } + } + return result + } + + const getClassname = (item, currentSelection) => { + if (item === currentSelection) return 'selected'; + return 'unselected'; + } + + let regions = getRegionList(data.getAllRegions) + return ( + + + {/* Panel to show regions */} + + + Regions in Continent {selectedContinent} (Count:{regions.length}) + + + + + {regions.map((item, key) => { + return handleRegionClick(item.countryRegion)}> + {item.countryRegion} + + }) + } + + + + + + {/* Panel to show countries */} + {/* */} + + + Countries filtered by selected region {selectedRegion} (Count:{countryData.length}) + + + + + {countryData.map((item, key) => { + return handleCountryClick(item)}> + {item.countryName} + + }) + } + + + + + + {/* Panel to show cities */} + + + Cities filtered by selected country {selectedCountry} + + + + { + selectedCountryCode && + } + + + + + ); +} + +export default AccordionGroup; diff --git a/ui/src/component/City.js b/ui/src/component/City.js new file mode 100644 index 0000000..8ec4404 --- /dev/null +++ b/ui/src/component/City.js @@ -0,0 +1,74 @@ +import React, { useState } from 'react' +import { FaRegTrashAlt } from 'react-icons/fa' +import { AiFillEdit } from 'react-icons/ai' +import ListGroup from 'react-bootstrap/ListGroup' +import { useQuery, NetworkStatus } from "@apollo/client" +import { useMutation } from '@apollo/client' +import { GET_CITIES, DELETE_CITY } from '../container/Query' +import UpdateModal from './Modal' +import Loading from './Loading' + +function City({ countryCode }) { + const [ show, setShow ] = useState(false) + const [ row, setRow ] = useState("") + + const [ deleteCity,{ loading: delMutationLoading, error: delMutationError } ] = useMutation(DELETE_CITY) + + const { loading, error, data, refetch, networkStatus } = useQuery(GET_CITIES, { + variables: {countryCode}, + fetchPolicy: "no-cache", + notifyOnNetworkStatusChange: true + }, + ); + + if (networkStatus === NetworkStatus.refetch) return 'Refetching!' && ; + if (loading) return null; + if (error) return `Error! ${error}`; + + + const toggleOpen = (item) => { + setShow(true) + setRow(item) + } + + const closeModal = () => { + refetch() + setShow(false) + } + + const handleDelete = (item) => { + deleteCity({ + variables: { + "cityId": `${item.cityId}`, + refetchQueries: [{query: GET_CITIES}] + } + }); + console.log("Deleted") + refetch() + + } + + const getClassname = (item, currentSelection) => { + if (item === currentSelection) return 'selected'; + return 'unselected'; + } + + return (<> + setShow(false)} handleClose={closeModal}> + {delMutationLoading &&

Loading after delete...

} + {delMutationError &&

Error :( Please try again

} + { + data && data.getAllCities && data.getAllCities.map((item, key) => + + {item.cityName} + + toggleOpen(item)} /> | {handleDelete(item)}} /> + + + ) + } + + ) +} + +export default City; diff --git a/ui/src/component/Loading.js b/ui/src/component/Loading.js new file mode 100644 index 0000000..a1c094b --- /dev/null +++ b/ui/src/component/Loading.js @@ -0,0 +1,8 @@ +import React from 'react'; +import {Spinner} from 'react-bootstrap' + +export default function Loading() { + return + Loading... + +} \ No newline at end of file diff --git a/ui/src/component/Modal.js b/ui/src/component/Modal.js index 83c92c7..d148c77 100644 --- a/ui/src/component/Modal.js +++ b/ui/src/component/Modal.js @@ -1,172 +1,146 @@ -import React, { useState } from 'react'; +import React, { useState } from 'react' import Modal from 'react-bootstrap/Modal' import Button from 'react-bootstrap/Button' -import InputGroup from 'react-bootstrap/InputGroup' import Form from 'react-bootstrap/Form' import FormControl from 'react-bootstrap/FormControl' import Col from 'react-bootstrap/Col' -import { gql,useMutation } from '@apollo/client'; -import { client } from '../index'; +import { useMutation } from '@apollo/client' +import { ADD_CITY, GET_CITIES } from '../container/Query' +const UpdateModal = ({ show, row, handleClose, handleCloseModal }) => { + const [ validated, setValidated ] = useState(false); + const [ addCity ] = useMutation(ADD_CITY); + const [ state, setState] = useState({ + id: null, + name: null, + code: null, + district: null, + population: null + }); -const UpdateModal=({show, handleClose, item}) =>{ - const [state, setState] = useState({ - id: item[0], - name: item[1], - code: item[2], - district: item[3], - population: item[4] - }); - const [validated, setValidated] = useState(false); + const handleSubmit = (event) => { + const form = event.currentTarget; - - const SET_MUTATION = gql ` - mutation{ - addCity(cityId:${state.id},cityName:"${state.name}",cityCountrycode:"${state.code}",cityDistrict:"${state.district}",cityPopulation:${state.population}) { - city{ - cityId, - cityName, - cityCountrycode, - cityDistrict, - cityPopulation - } - } - }`; - const DELETE_MUTATION = gql` - mutation{ - deleteCity(cityId:${item[0]}) { - city{ - cityId + if (form.checkValidity() === false) { + event.preventDefault(); + event.stopPropagation(); + } + else { + setValidated(true); + addCity({ + variables: { + "cityId": `${state.id}`, + "cityName": `${state.name}`, + "cityCountrycode": `${state.code}`, + "cityDistrict": `${state.district}`, + "cityPopulation": `${state.population}`, + refetchQueries: [{query: GET_CITIES}] } - } - }`; - const [ addCity ] = useMutation(SET_MUTATION); - const [ deleteCity, { loading, error } ] = useMutation(DELETE_MUTATION); - - if (loading) return

Loading

;; - if (error) return

An error occurred

; - - const handleSubmit = (event) => { - const form = event.currentTarget; - - if (form.checkValidity() === false) { - event.preventDefault(); - event.stopPropagation(); - } else { - setValidated(true); - addCity(); - handleClose(); - } - }; - - - const handleDelete = (event) => { - deleteCity() - handleClose() - } + }); + handleClose(); + } + }; - return ( - <> - - - Admin Panel - - Woohoo, you can now edit the city records! -
- - Id - { - let inputValue = e.target.value; - (setState({ - ...state, - id: inputValue - } - ))}} - /> - + return ( + <> + + + Admin Panel + + Woohoo, you can now edit the city records! + + + Id + { + let inputValue = e.target.value; + (setState({ + ...state, + id: inputValue + } + )) + }}/> + - - Name - + Name + - { let inputValue = e.target.value; - setState({ - ...state, - name: inputValue - })}} - /> - + onChange={e => { + let inputValue = e.target.value; + setState({ + ...state, + name: inputValue + }) + }} + /> + - + Code - - { let inputValue = e.target.value; + { + let inputValue = e.target.value; setState({ - ...state, - code:inputValue - })}} + ...state, + code: inputValue + }) + }} /> - + - + District { + required + placeholder={row && row.cityDistrict} + aria-label="district" + aria-describedby="district" + onChange={e => { let inputValue = e.target.value; setState({ - ...state, - district: inputValue - })}} + ...state, + district: inputValue + }) + }} /> - + - + Population - { let inputValue = e.target.value; + required + placeholder={row && row.cityPopulation} + aria-label="population" + aria-describedby="population" + onChange={e => { + let inputValue = e.target.value; setState({ - ...state, - population: inputValue - })}} - /> - + ...state, + population: inputValue + }) + }} + /> + - - + - -
- - ); - } - - export default UpdateModal; + +
+ + ); +} + +export default UpdateModal; diff --git a/ui/src/component/WorldlMap.js b/ui/src/component/WorldlMap.js index ce1d56c..996c676 100644 --- a/ui/src/component/WorldlMap.js +++ b/ui/src/component/WorldlMap.js @@ -25,7 +25,7 @@ export default class WorldMap extends React.Component { return
`Continent: ${dataTip}`} /> - + diff --git a/ui/src/container/MapPage.js b/ui/src/container/MapPage.js index c9d0d25..d7a292f 100644 --- a/ui/src/container/MapPage.js +++ b/ui/src/container/MapPage.js @@ -2,27 +2,12 @@ import React, { useState } from 'react'; import styled, { keyframes } from "styled-components"; import WorldMap from '../component/WorldlMap'; import Footer from '../component/Footer'; -import ListGroup from 'react-bootstrap/ListGroup' -import Accordion from 'react-bootstrap/Accordion' -import Card from 'react-bootstrap/Card' -import { client } from '../index'; -import { gql } from '@apollo/client'; -import { FaRegTrashAlt } from 'react-icons/fa'; -import { AiFillEdit } from 'react-icons/ai'; -import UpdateModal from '../component/Modal' +import AccordionGroup from '../component/AccordionGroup' import Row from 'react-bootstrap/Row' import Col from 'react-bootstrap/Col' -import Map from 'collections/map'; -import Button from 'react-bootstrap/esm/Button'; const Page = styled.div` - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - color: black; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @@ -63,6 +48,17 @@ const MapPageElm = styled(Page)` animation: ${slideOutRight} 0.2s forwards; } + .accordion { + padding-top: 2rem; + padding-bottom: 2rem; + } + .card-align { + float: right; + font-size: 0.8rem; + } + + + .map-selected { fill: #E3DA37; } @@ -75,6 +71,10 @@ const MapPageElm = styled(Page)` cursor: pointer; } + .map-img { + width: 100% + } + .map-unselected:hover{ fill: blanchedalmond; } @@ -101,29 +101,34 @@ const MapPageElm = styled(Page)` } .mapPage-footer { - position: fixed; color: lightslategray; + position: fixed; bottom: 0; font-size: 0.8rem; width: 100%; text-align: center; } + @media only screen and (max-width: 640px) + { + .mapPage-footer { + display: none !important; + } + } .card-header { cursor: pointer; } - .card-align { - float: right; - font-size: 0.8rem; - } - .card-body { overflow: scroll; max-height: 500px; cursor: pointer; } + .selected { + background-color: #E3DA37; + } + .list-group-item:hover { background-color: lightyellow; } @@ -140,221 +145,38 @@ function MapPage() { 'AF': 'Africa', 'EU': 'Europe' } - - const [state, setState] = useState({ - selectedContinentCode: null, - selectedRegion: null, - selectedCountry: null, - selectedCity: null, - regionsList: [], - countriesMap: new Map(), - citiesMap: new Map(), - item: '', - show: false - }) - - const toggleClose = item => setState({ - ...state, - selectedCity: null, - selectedCountry: null, - citiesMap: new Map(), - item: '', - show: false - }) - - const toggleShow = item => { - setState({ - ...state, - item: item, - show: true - }) - } + const [selectedContinentCode, setSelectedContinentCode] = useState(null) /* ************************************************************ */ /* Get countries on map click /* ************************************************************ */ const handleMapClick = continentCode => { - const GET_REGIONS = gql` - query { - getAllCountries(countryContinent:"${worldMap[continentCode]}"){ - countryRegion, - countryCode - } - }`; - client.query({ query: GET_REGIONS }).then(result => { - const uniqueRegionList = [...new Set(result.data.getAllCountries.map(item => item.countryRegion))]; - setState({ - ...state, - selectedContinentCode: continentCode, - selectedCountry: null, - selectedCity: null, - selectedRegion: null, - countriesMap: new Map(), - citiesMap: new Map(), - regionsList: uniqueRegionList, - }) - }); - } - - /* ************************************************************ */ - /* Get regions on selected country - /* ************************************************************ */ - function handleRegionClick(e) { - let countryMap = new Map(), - selectedRegion = e.target.textContent - const GET_COUNTRIES = gql` - query { - getAllRegions(countryRegion:"${e.target.textContent}"){ - countryName, - countryCode - } - }`; - client.query({ - query: GET_COUNTRIES - }).then(result => { - result.data.getAllRegions.forEach(item => { - if (!countryMap.has(item.countryCode)) - return countryMap.set(item.countryCode, item.countryName) - }); - setState({ - ...state, - selectedRegion: selectedRegion, - selectedCountry: '', - countriesMap: countryMap, - citiesMap: new Map(), - selectedCity: '' - }) - }); - } - - /* ************************************************************ */ - /* Get cities on selected country based on the country code - /* ************************************************************ */ - function handleCountryClick(e) { - let countryCode = e.target.id, - selectedCountry = e.target.textContent, - cityMap = new Map() - const GET_CITIES = gql` - query{ - getAllCities(cityCountrycode:"${countryCode}"){ - cityId, - cityName, - cityCountrycode, - cityDistrict, - cityPopulation - } - }`; - client.query({ query: GET_CITIES }).then(result => { - result.data.getAllCities.forEach(item => { - if (!cityMap.has(item.cityId)) - cityMap.set(item.cityId, [item.cityId, item.cityName, item.cityCountrycode, item.cityDistrict, item.cityPopulation]) - }); - setState({ - ...state, - selectedCountry: selectedCountry, - citiesMap: cityMap, - }) - }); - - // To get country data - // const GET_COUNTRYDATA = gql` - // query{ - // countrylanguage(countrylanguageCountrycode:"${countryCode}"){ - // countrylanguageIsofficial, - // countrylanguageLanguage, - // countrylanguagePercentage - // } - // }`; - - // client.query({ - // query: GET_COUNTRYDATA - // }).then(result => { - // result.data.countrylanguage.forEach(item => { - // //console.log("item", item) - // }); - // }); + console.log("**************handleMapClick continentCode", continentCode) + setSelectedContinentCode(continentCode) } return ( - - Click the map to populate the information under the accordions + + Click the map to populate the information under the accordions - - + + - + - - - {/* Panel to show regions */} - - - Regions in Continent {worldMap[state.selectedContinentCode]} (Count:{state.regionsList.length}) - - - - - { - state.regionsList.map(((item, val) => - - {item} - - )) - } - - - - - - {/* Panel to show countries */} - {/* */} - - - Countries filtered by selected region {state.selectedRegion} (Count:{state.countriesMap.size}) - - - - Make a selection - { - state.countriesMap.map(((item, val) => - - {item} - - )) - } - - - - - {/* Panel to show cities */} - - - Cities filtered by selected country {state.selectedCountry} (Count:{state.citiesMap.size}) - - - - Make a selection again - {state.citiesMap.map(((item, val) => toggleShow(item)} > - {item[1]} - | - - - )) - } - - - - - + +