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 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
|
||||
@ -19,6 +19,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);
|
||||
|
||||
|
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 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<AppProps, AppState> {
|
||||
<Route exact path="/data-accuracy.html" component={DataAccuracyPage} />
|
||||
<Route exact path="/data-extracts.html" component={DataExtracts} />
|
||||
<Route exact path="/contact.html" component={ContactPage} />
|
||||
<Route exact path="/leaderboard.html" component={LeaderboardPage} />
|
||||
<Route exact path="/history.html" component={ChangesPage} />
|
||||
<Route exact path={App.mapAppPaths} render={(props) => (
|
||||
<MapApp
|
||||
|
@ -89,6 +89,11 @@ class Header extends React.Component<HeaderProps, HeaderState> {
|
||||
Downloads
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/leaderboard.html" className="nav-link" onClick={this.handleNavigate}>
|
||||
Leaderboard
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink to="/contact.html" className="nav-link" onClick={this.handleNavigate}>
|
||||
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