Merge pull request #535 from colouring-london/feature/initial_leaderboard
Implementation of basic leaderboard
This commit is contained in:
commit
b782f36230
@ -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);
|
||||||
|
|
||||||
|
22
app/src/api/controllers/leaderboardController.ts
Normal file
22
app/src/api/controllers/leaderboardController.ts
Normal 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
|
||||||
|
};
|
9
app/src/api/routes/leaderboardRouter.ts
Normal file
9
app/src/api/routes/leaderboardRouter.ts
Normal 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;
|
39
app/src/api/services/leaderboard.ts
Normal file
39
app/src/api/services/leaderboard.ts
Normal 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
|
||||||
|
};
|
@ -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
|
||||||
|
@ -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
|
||||||
|
34
app/src/frontend/pages/leaderboard.css
Normal file
34
app/src/frontend/pages/leaderboard.css
Normal 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;
|
||||||
|
}
|
149
app/src/frontend/pages/leaderboard.tsx
Normal file
149
app/src/frontend/pages/leaderboard.tsx
Normal 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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user