From 1f2256c721f127ce71a8f8bb1f0c08f7a600dec8 Mon Sep 17 00:00:00 2001 From: Martin-dJ Date: Mon, 16 Dec 2019 13:26:13 +0000 Subject: [PATCH 1/2] Implementation of basic leaderboard --- app/src/api/api.ts | 3 +- .../api/controllers/leaderboardController.ts | 22 +++ app/src/api/routes/leaderboardRouter.ts | 9 ++ app/src/api/services/leaderboard.ts | 39 +++++ app/src/frontend/app.tsx | 2 + app/src/frontend/header.tsx | 5 + app/src/frontend/pages/leaderboard.css | 34 ++++ app/src/frontend/pages/leaderboard.tsx | 149 ++++++++++++++++++ 8 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 app/src/api/controllers/leaderboardController.ts create mode 100644 app/src/api/routes/leaderboardRouter.ts create mode 100644 app/src/api/services/leaderboard.ts create mode 100644 app/src/frontend/pages/leaderboard.css create mode 100644 app/src/frontend/pages/leaderboard.tsx diff --git a/app/src/api/api.ts b/app/src/api/api.ts index 59471b6b..d4650a47 100644 --- a/app/src/api/api.ts +++ b/app/src/api/api.ts @@ -4,11 +4,11 @@ import express from 'express'; import * as editHistoryController from './controllers/editHistoryController'; import buildingsRouter from './routes/buildingsRouter'; import extractsRouter from './routes/extractsRouter'; +import leaderboardRouter from './routes/leaderboardRouter'; import usersRouter from './routes/usersRouter'; import { queryLocation } from './services/search'; import { authUser, getNewUserAPIKey, logout } from './services/user'; - const server = express.Router(); // parse POSTed json body @@ -17,6 +17,7 @@ server.use(bodyParser.json()); server.use('/buildings', buildingsRouter); server.use('/users', usersRouter); server.use('/extracts', extractsRouter); +server.use('/leaderboard', leaderboardRouter); server.get('/history', editHistoryController.getGlobalEditHistory); diff --git a/app/src/api/controllers/leaderboardController.ts b/app/src/api/controllers/leaderboardController.ts new file mode 100644 index 00000000..06024597 --- /dev/null +++ b/app/src/api/controllers/leaderboardController.ts @@ -0,0 +1,22 @@ +import express from 'express'; + +import asyncController from "../routes/asyncController"; +import * as leadersService from '../services/leaderboard'; + +const getLeaders = asyncController(async (req: express.Request, res: express.Response) => { + try { + const number_limit = req.query.number_limit; + const time_limit = req.query.time_limit; + const result = await leadersService.getLeaders(number_limit, time_limit); + res.send({ + leaders: result + }); + } catch(error) { + console.error(error); + res.send({ error: 'Database error' }); + } +}); + +export default{ + getLeaders +}; diff --git a/app/src/api/routes/leaderboardRouter.ts b/app/src/api/routes/leaderboardRouter.ts new file mode 100644 index 00000000..e0e17c31 --- /dev/null +++ b/app/src/api/routes/leaderboardRouter.ts @@ -0,0 +1,9 @@ +import express from 'express'; + +import leaderboardController from '../controllers/leaderboardController'; + +const router = express.Router(); + +router.get('/leaders', leaderboardController.getLeaders); + +export default router; diff --git a/app/src/api/services/leaderboard.ts b/app/src/api/services/leaderboard.ts new file mode 100644 index 00000000..6eb407b7 --- /dev/null +++ b/app/src/api/services/leaderboard.ts @@ -0,0 +1,39 @@ +import db from '../../db'; + +async function getLeaders(number_limit: number, time_limit: number) { + try { + if(time_limit > 0){ + return await db.manyOrNone( + `SELECT count(log_id) as number_edits, username + FROM logs, users + WHERE logs.user_id=users.user_id + AND CURRENT_TIMESTAMP::DATE - log_timestamp::DATE <= $1 + AND NOT (users.username = 'casa_friendly_robot') + AND NOT (users.username = 'colouringlondon') + GROUP by users.username + ORDER BY number_edits DESC + LIMIT $2`, [number_limit, time_limit] + ); + + }else{ + return await db.manyOrNone( + `SELECT count(log_id) as number_edits, username + FROM logs, users + WHERE logs.user_id=users.user_id + AND NOT (users.username = 'casa_friendly_robot') + AND NOT (users.username = 'colouringlondon') + GROUP by users.username + ORDER BY number_edits DESC + LIMIT $1`, [number_limit] + ); + } + } catch(error) { + console.error(error); + return []; + } +} + + +export { + getLeaders +}; diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 6d39803d..2cd49b9d 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -14,6 +14,7 @@ import ContactPage from './pages/contact'; import ContributorAgreementPage from './pages/contributor-agreement'; import DataAccuracyPage from './pages/data-accuracy'; import DataExtracts from './pages/data-extracts'; +import LeaderboardPage from './pages/leaderboard'; import OrdnanceSurveyLicencePage from './pages/ordnance-survey-licence'; import OrdnanceSurveyUprnPage from './pages/ordnance-survey-uprn'; import PrivacyPolicyPage from './pages/privacy-policy'; @@ -113,6 +114,7 @@ class App extends React.Component { + ( { Downloads +
  • + + Leaderboard + +
  • Contact diff --git a/app/src/frontend/pages/leaderboard.css b/app/src/frontend/pages/leaderboard.css new file mode 100644 index 00000000..e89a487f --- /dev/null +++ b/app/src/frontend/pages/leaderboard.css @@ -0,0 +1,34 @@ +table { + table-layout: fixed; + width: 60%; + margin-left: 20%; + margin-right: 20%; + border: 1px solid black; +} + +table th, td { + border: 1px solid black; + text-align: left; + padding-left: 1%; +} + +table tr:nth-child(odd) { + background: #f6f8fa; +} + +table tr:nth-child(1) { + background: #fff; +} + +#title { + text-align: center; + padding-bottom: 1%; +} + +#radiogroup { + padding: 1%; +} + +input[type="radio"] { + margin: 0 2px 0 10px; +} diff --git a/app/src/frontend/pages/leaderboard.tsx b/app/src/frontend/pages/leaderboard.tsx new file mode 100644 index 00000000..7951f391 --- /dev/null +++ b/app/src/frontend/pages/leaderboard.tsx @@ -0,0 +1,149 @@ +import React, { Component } from 'react'; + +import './leaderboard.css'; + + interface Leader { + number_edits: string; + username: string; + } + + + interface LeaderboardProps { + } + + + interface LeaderboardState { + leaders: Leader[]; + fetching: boolean; + + //We need to track the state of the radio buttons to ensure their current state is shown correctly when the view is (re)rendered + number_limit: number; + time_limit: number; + } + + +class LeaderBoardPage extends Component { + + constructor(props) { + super(props); + this.state = { + leaders: [], + fetching: false, + number_limit: 10, + time_limit: -1 + }; + this.getLeaders = this.getLeaders.bind(this); + this.renderTableData = this.renderTableData.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + + handleChange(e) { + if(e.target.name == 'number_limit'){ + this.getLeaders(e.target.value, this.state.time_limit); + this.setState({number_limit: e.target.value}); + }else { + this.getLeaders(this.state.number_limit, e.target.value); + this.setState({time_limit: e.target.value}); + } + } + + + componentDidMount() { + this.getLeaders(this.state.number_limit, this.state.time_limit); + } + + + componentWillUnmount() {} + + + getLeaders(number_limit, time_limit) { + + this.setState({ + fetching: true + }); + + fetch( + '/api/leaderboard/leaders?number_limit=' + number_limit + '&time_limit='+time_limit + ).then( + (res) => res.json() + ).then((data) => { + if (data && data.leaders){ + this.setState({ + leaders: data.leaders, + fetching: false + }); + + } else { + console.error(data); + + this.setState({ + leaders: [], + fetching: false + }); + } + }).catch((err) => { + console.error(err); + + this.setState({ + leaders: [], + fetching: false + }); + }); + } + + + renderTableData() { + return this.state.leaders.map((u, i) => { + const username = u.username; + const number_edits = u.number_edits; + + return ( + + {i+1} + {username} + {number_edits} + + ); + }); + } + + + render() { + return( +
    +
    +
    +

    Select number of users to be displayed:
    + 10 + 100 +

    +
    +
    +

    Select time period:
    + All time + Last 7 days + Last 30 days +

    +
    +
    +

    Leader Board

    + + + + + + + + {this.renderTableData()} + +
    RankUsernameContributions
    +
    + ); + } + +} + + +export default LeaderBoardPage; + From 08cc107388f54ec7316707f2c444594afea664d5 Mon Sep 17 00:00:00 2001 From: Martin-dJ Date: Mon, 6 Jan 2020 18:10:42 +0000 Subject: [PATCH 2/2] Fixed issue with database query and implemented review suggestions --- app/src/api/services/leaderboard.ts | 2 +- app/src/frontend/pages/leaderboard.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/api/services/leaderboard.ts b/app/src/api/services/leaderboard.ts index 6eb407b7..52d6191c 100644 --- a/app/src/api/services/leaderboard.ts +++ b/app/src/api/services/leaderboard.ts @@ -12,7 +12,7 @@ async function getLeaders(number_limit: number, time_limit: number) { AND NOT (users.username = 'colouringlondon') GROUP by users.username ORDER BY number_edits DESC - LIMIT $2`, [number_limit, time_limit] + LIMIT $2`, [time_limit, number_limit] ); }else{ diff --git a/app/src/frontend/pages/leaderboard.tsx b/app/src/frontend/pages/leaderboard.tsx index 7951f391..5ec6612d 100644 --- a/app/src/frontend/pages/leaderboard.tsx +++ b/app/src/frontend/pages/leaderboard.tsx @@ -22,7 +22,7 @@ import './leaderboard.css'; } -class LeaderBoardPage extends Component { +class LeaderboardPage extends Component { constructor(props) { super(props); @@ -99,7 +99,7 @@ class LeaderBoardPage extends Component { const number_edits = u.number_edits; return ( - + {i+1} {username} {number_edits} @@ -145,5 +145,5 @@ class LeaderBoardPage extends Component { } -export default LeaderBoardPage; +export default LeaderboardPage;