Merge pull request #535 from colouring-london/feature/initial_leaderboard

Implementation of basic leaderboard
This commit is contained in:
Tom Russell 2020-04-09 10:33:23 +01:00 committed by GitHub
commit b782f36230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 262 additions and 1 deletions

View File

@ -6,11 +6,11 @@ import { ApiParamError, ApiUserError } from './errors/api';
import { DatabaseError } from './errors/general'; import { DatabaseError } from './errors/general';
import buildingsRouter from './routes/buildingsRouter'; import buildingsRouter from './routes/buildingsRouter';
import extractsRouter from './routes/extractsRouter'; import extractsRouter from './routes/extractsRouter';
import leaderboardRouter from './routes/leaderboardRouter';
import usersRouter from './routes/usersRouter'; import usersRouter from './routes/usersRouter';
import { queryLocation } from './services/search'; import { queryLocation } from './services/search';
import { authUser, getNewUserAPIKey, logout } from './services/user'; import { authUser, getNewUserAPIKey, logout } from './services/user';
const server = express.Router(); const server = express.Router();
// parse POSTed json body // parse POSTed json body
@ -19,6 +19,7 @@ server.use(bodyParser.json());
server.use('/buildings', buildingsRouter); server.use('/buildings', buildingsRouter);
server.use('/users', usersRouter); server.use('/users', usersRouter);
server.use('/extracts', extractsRouter); server.use('/extracts', extractsRouter);
server.use('/leaderboard', leaderboardRouter);
server.get('/history', editHistoryController.getGlobalEditHistory); server.get('/history', editHistoryController.getGlobalEditHistory);

View File

@ -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
};

View File

@ -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;

View File

@ -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`, [time_limit, number_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
};

View File

@ -14,6 +14,7 @@ import ContactPage from './pages/contact';
import ContributorAgreementPage from './pages/contributor-agreement'; import ContributorAgreementPage from './pages/contributor-agreement';
import DataAccuracyPage from './pages/data-accuracy'; import DataAccuracyPage from './pages/data-accuracy';
import DataExtracts from './pages/data-extracts'; import DataExtracts from './pages/data-extracts';
import LeaderboardPage from './pages/leaderboard';
import OrdnanceSurveyLicencePage from './pages/ordnance-survey-licence'; import OrdnanceSurveyLicencePage from './pages/ordnance-survey-licence';
import OrdnanceSurveyUprnPage from './pages/ordnance-survey-uprn'; import OrdnanceSurveyUprnPage from './pages/ordnance-survey-uprn';
import PrivacyPolicyPage from './pages/privacy-policy'; import PrivacyPolicyPage from './pages/privacy-policy';
@ -113,6 +114,7 @@ class App extends React.Component<AppProps, AppState> {
<Route exact path="/data-accuracy.html" component={DataAccuracyPage} /> <Route exact path="/data-accuracy.html" component={DataAccuracyPage} />
<Route exact path="/data-extracts.html" component={DataExtracts} /> <Route exact path="/data-extracts.html" component={DataExtracts} />
<Route exact path="/contact.html" component={ContactPage} /> <Route exact path="/contact.html" component={ContactPage} />
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
<Route exact path="/history.html" component={ChangesPage} /> <Route exact path="/history.html" component={ChangesPage} />
<Route exact path={App.mapAppPaths} render={(props) => ( <Route exact path={App.mapAppPaths} render={(props) => (
<MapApp <MapApp

View File

@ -89,6 +89,11 @@ class Header extends React.Component<HeaderProps, HeaderState> {
Downloads Downloads
</NavLink> </NavLink>
</li> </li>
<li className="nav-item">
<NavLink to="/leaderboard.html" className="nav-link" onClick={this.handleNavigate}>
Leaderboard
</NavLink>
</li>
<li className="nav-item"> <li className="nav-item">
<NavLink to="/contact.html" className="nav-link" onClick={this.handleNavigate}> <NavLink to="/contact.html" className="nav-link" onClick={this.handleNavigate}>
Contact Contact

View File

@ -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;
}

View File

@ -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<LeaderboardProps, LeaderboardState> {
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 (
<tr key={username}>
<td>{i+1}</td>
<td>{username}</td>
<td>{number_edits}</td>
</tr>
);
});
}
render() {
return(
<div>
<form id="radiogroup">
<div id="number-radiogroup" >
<p>Select number of users to be displayed: <br/>
<input type="radio" name="number_limit" value="10" onChange={this.handleChange} checked={10 == this.state.number_limit} />10
<input type="radio" name="number_limit" value="100" onChange={this.handleChange} checked={100 == this.state.number_limit} />100
</p>
</div>
<div id="time-radiogroup" >
<p>Select time period: <br/>
<input type="radio" name="time_limit" value="-1" onChange={this.handleChange} checked={-1 == this.state.time_limit} /> All time
<input type="radio" name="time_limit" value="7" onChange={this.handleChange} checked={7 == this.state.time_limit} /> Last 7 days
<input type="radio" name="time_limit" value="30" onChange={this.handleChange} checked={30 == this.state.time_limit} /> Last 30 days
</p>
</div>
</form>
<h1 id='title'>Leader Board</h1>
<table id='leaderboard'>
<tbody>
<tr>
<th>Rank</th>
<th>Username</th>
<th>Contributions</th>
</tr>
{this.renderTableData()}
</tbody>
</table>
</div>
);
}
}
export default LeaderboardPage;