Add statistics about the instance under the About section.

This commit is contained in:
Lucas Verney 2018-07-31 17:04:06 +02:00
parent aca68fb2e3
commit d2bae9e532
7 changed files with 94 additions and 5 deletions

View File

@ -17,7 +17,11 @@ could know the location of the displayed map.
The data collected by https://cyclo.phyks.me/ is available under an The data collected by https://cyclo.phyks.me/ is available under an
[ODbL](https://opendatacommons.org/licenses/odbl/) license. You can get the [ODbL](https://opendatacommons.org/licenses/odbl/) license. You can get the
most up to date JSON dump of available reports at https://cyclo.phyks.me/api/v1/reports. most up to date JSON dump of available reports at
https://cyclo.phyks.me/api/v1/reports.
Statistics about the instance can be fetched at
https://cyclo.phyks.me/api/v1/stats.
## Hosting your own ## Hosting your own
@ -78,6 +82,9 @@ python -m server
It is better to use a dedicated `virtualenv` if you can :) It is better to use a dedicated `virtualenv` if you can :)
API routes are all listed within `server/routes.py` file, with documentation
strings.
#### Useful environment variables #### Useful environment variables
You can pass a few environment variables to the `python -m server` command to You can pass a few environment variables to the `python -m server` command to

View File

@ -264,3 +264,34 @@ def downvote_report(id):
return { return {
"data": r.to_json() "data": r.to_json()
} }
@bottle.route('/api/v1/stats', ["GET", "OPTIONS"])
def get_stats():
"""
API v1 GET stats about this instance.
Example::
GET /api/v1/states
"""
# Handle CORS
if bottle.request.method == 'OPTIONS':
return {}
nb_reports = Report.select().count()
nb_active_reports = Report.select().where(
(Report.expiration_datetime == None) |
(Report.expiration_datetime > arrow.utcnow().replace(microsecond=0).datetime)
).count()
last_added_report_datetime = Report.select().order_by(
Report.datetime.desc()
).get().datetime
return {
"data": {
"nb_reports": nb_reports,
"nb_active_reports": nb_active_reports,
"last_added_report_datetime": last_added_report_datetime
}
}

View File

@ -36,6 +36,16 @@ export function getActiveReports() {
}); });
} }
export function getStats() {
return fetch(`${BASE_URL}api/v1/stats`)
.then(response => response.json())
.then(response => response.data)
.catch((exc) => {
console.error(`Unable to fetch stats: ${exc}.`);
throw exc;
});
}
export function downvote(id) { export function downvote(id) {
return fetch(`${BASE_URL}api/v1/reports/${id}/downvote`, { return fetch(`${BASE_URL}api/v1/reports/${id}/downvote`, {
method: 'POST', method: 'POST',

View File

@ -12,7 +12,7 @@
{{ report.label }} {{ report.label }}
</v-flex> </v-flex>
<v-flex xs12 class="secondLine"> <v-flex xs12 class="secondLine">
{{ $t('reportCard.Reported') }} {{ report.fromNow }} {{ $t('reportCard.Reported', { fromNow: report.fromNow }) }}
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-flex> </v-flex>

View File

@ -1,7 +1,12 @@
{ {
"about": { "about": {
"availableReportsTitle": "The available reports so far are:", "availableReportsTitle": "The available reports so far are:",
"license": "It is released under an <a href=\"https://opensource.org/licenses/MIT\">MIT license</a> (<a href=\"https://framagit.org/phyks/cyclassist\">source code</a>). Icons are based on creations from Wikimedia, Vecteezy, Pixabay or Flaticon. Sounds are based on CC0 works from <a href=\"https://freesound.org/\">freesound.org</a>. The map background is using tiles from <a href=\"https://carto.com/location-data-services/basemaps/\">Carto.com</a> or <a href=\"http://thunderforest.com/\">Thunderforest</a>, thanks to <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap contributors</a> and <a href=\"http://leafletjs.com/\">Leaflet</a>. Collected reports are available under <a href=\"https://opendatacommons.org/licenses/odbl/\">ODbL license</a>. Manual location picking uses the awesome API from <a href=\"https://adresse.data.gouv.fr\">adresse.data.gouv.fr</a>.", "lastReportAdded": "Last report added {fromNow}.",
"license": "License",
"licenseDescription": "It is released under an <a href=\"https://opensource.org/licenses/MIT\">MIT license</a> (<a href=\"https://framagit.org/phyks/cyclassist\">source code</a>). Icons are based on creations from Wikimedia, Vecteezy, Pixabay or Flaticon. Sounds are based on CC0 works from <a href=\"https://freesound.org/\">freesound.org</a>. The map background is using tiles from <a href=\"https://carto.com/location-data-services/basemaps/\">Carto.com</a> or <a href=\"http://thunderforest.com/\">Thunderforest</a>, thanks to <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap contributors</a> and <a href=\"http://leafletjs.com/\">Leaflet</a>. Collected reports are available under <a href=\"https://opendatacommons.org/licenses/odbl/\">ODbL license</a>. Manual location picking uses the awesome API from <a href=\"https://adresse.data.gouv.fr\">adresse.data.gouv.fr</a>.",
"nbActiveReports": "No active report. | One active report. | {nbActiveReports} active reports.",
"nbReports": "No report. | One report. | {nbReports} reports.",
"stats": "Stats",
"summary": "This app lets you track and share issues with bike lanes.", "summary": "This app lets you track and share issues with bike lanes.",
"usage": "How to use", "usage": "How to use",
"usageDescription": "Use the button in the lower right corner to add a new report at your current location. To add a report elsewhere, do a click where you want the report to be shown. Press on a marker on the map to display more informations and report the problem as being still there or solved." "usageDescription": "Use the button in the lower right corner to add a new report at your current location. To add a report elsewhere, do a click where you want the report to be shown. Press on a marker on the map to display more informations and report the problem as being still there or solved."
@ -61,7 +66,7 @@
"vibrate": "Vibrate" "vibrate": "Vibrate"
}, },
"reportCard": { "reportCard": {
"Reported": "Reported" "Reported": "Reported {fromNow}."
}, },
"reportDialog": { "reportDialog": {
"unableToSendDescription": "There was a network issue preventing from sending the latest report.", "unableToSendDescription": "There was a network issue preventing from sending the latest report.",

View File

@ -10,18 +10,52 @@
<h2 class="body-2">{{ $t('about.availableReportsTitle') }}</h2> <h2 class="body-2">{{ $t('about.availableReportsTitle') }}</h2>
<ReportsDescription></ReportsDescription> <ReportsDescription></ReportsDescription>
<p class="mt-3" v-html="$t('about.license')"></p> <h2 class="body-2 mt-3">{{ $t('about.stats') }}</h2>
<v-progress-circular indeterminate v-if="!stats"></v-progress-circular>
<ul v-else>
<li>{{ $tc('about.nbActiveReports', stats.nb_active_reports, { nbActiveReports: stats.nb_active_reports }) }}</li>
<li>{{ $tc('about.nbReports', stats.nb_reports, { nbReports: stats.nb_reports }) }}</li>
<li>{{ $t('about.lastReportAdded', { fromNow: stats.last_added_report_datetime }) }}</li>
</ul>
<h2 class="body-2 mt-3">{{ $t('about.license') }}</h2>
<p v-html="$t('about.licenseDescription')"></p>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
</template> </template>
<script> <script>
import moment from 'moment';
import { getStats } from '@/api';
import ReportsDescription from '@/components/ReportsDescription.vue'; import ReportsDescription from '@/components/ReportsDescription.vue';
export default { export default {
components: { components: {
ReportsDescription, ReportsDescription,
}, },
created() {
this.fetchData();
},
data() {
return {
stats: null,
};
},
methods: {
fetchData() {
getStats().then((stats) => {
this.stats = stats;
this.stats.last_added_report_datetime = (
moment(this.stats.last_added_report_datetime).fromNow()
);
});
},
},
watch: {
$route: 'fetchData',
},
}; };
</script> </script>

View File

@ -12,6 +12,7 @@ import VGrid from 'vuetify/es5/components/VGrid';
import VIcon from 'vuetify/es5/components/VIcon'; import VIcon from 'vuetify/es5/components/VIcon';
import VList from 'vuetify/es5/components/VList'; import VList from 'vuetify/es5/components/VList';
import VMenu from 'vuetify/es5/components/VMenu'; import VMenu from 'vuetify/es5/components/VMenu';
import VProgressCircular from 'vuetify/es5/components/VProgressCircular';
import VProgressLinear from 'vuetify/es5/components/VProgressLinear'; import VProgressLinear from 'vuetify/es5/components/VProgressLinear';
import VSelect from 'vuetify/es5/components/VSelect'; import VSelect from 'vuetify/es5/components/VSelect';
import VSwitch from 'vuetify/es5/components/VSwitch'; import VSwitch from 'vuetify/es5/components/VSwitch';
@ -32,6 +33,7 @@ Vue.use(Vuetify, {
VIcon, VIcon,
VList, VList,
VMenu, VMenu,
VProgressCircular,
VProgressLinear, VProgressLinear,
VSelect, VSelect,
VSwitch, VSwitch,