Show the reports as soon as they are added in the db

This commit is contained in:
Lucas Verney 2018-06-26 11:39:43 +02:00
parent e961a8dbb1
commit f30d000f92
14 changed files with 109 additions and 832 deletions

View File

@ -23,7 +23,8 @@
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue2-leaflet": "^1.0.2", "vue2-leaflet": "^1.0.2",
"vue2-leaflet-tracksymbol": "^1.0.10", "vue2-leaflet-tracksymbol": "^1.0.10",
"vuetify": "^1.0.0" "vuetify": "^1.0.0",
"vuex": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^7.1.2", "autoprefixer": "^7.1.2",

View File

@ -1,12 +1,24 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding: utf-8 # coding: utf-8
import functools
import json
import bottle import bottle
from server import routes from server import routes
from server.jsonapi import DateAwareJSONEncoder
from server.models import db, Report from server.models import db, Report
if __name__ == "__main__": if __name__ == "__main__":
db.connect() db.connect()
db.create_tables([Report]) db.create_tables([Report])
# Use DateAwareJSONEncoder to dump JSON strings
# From http://stackoverflow.com/questions/21282040/bottle-framework-how-to-return-datetime-in-json-response#comment55718456_21282666. pylint: disable=locally-disabled,line-too-long
bottle.install(
bottle.JSONPlugin(
json_dumps=functools.partial(json.dumps, cls=DateAwareJSONEncoder)
)
)
bottle.run(host='0.0.0.0', port='8081') bottle.run(host='0.0.0.0', port='8081')

View File

@ -3,6 +3,7 @@
""" """
Helpers to implement a JSON API with Bottle. Helpers to implement a JSON API with Bottle.
""" """
import datetime
import json import json
import re import re
@ -11,6 +12,16 @@ import bottle
FILTER_RE = re.compile(r"filter\[([A-z0-9_]+)\]") FILTER_RE = re.compile(r"filter\[([A-z0-9_]+)\]")
class DateAwareJSONEncoder(json.JSONEncoder):
"""
Extend the default JSON encoder to serialize datetimes to iso strings.
"""
def default(self, o): # pylint: disable=locally-disabled,E0202
if isinstance(o, (datetime.date, datetime.datetime)):
return o.isoformat()
return json.JSONEncoder.default(self, o)
@bottle.hook('after_request') @bottle.hook('after_request')
def enable_cors(): def enable_cors():
""" """

View File

@ -38,4 +38,4 @@ class Report(BaseModel):
k: v for k, v in model_to_dict(self).items() k: v for k, v in model_to_dict(self).items()
if k != "id" if k != "id"
} }
} }

View File

@ -93,7 +93,7 @@ def post_report():
return jsonapi.JsonApiError(400, "Invalid JSON payload: " + str(exc)) return jsonapi.JsonApiError(400, "Invalid JSON payload: " + str(exc))
try: try:
Report.create( r = Report.create(
type=payload['type'], type=payload['type'],
lat=payload['lat'], lat=payload['lat'],
lng=payload['lng'] lng=payload['lng']
@ -102,5 +102,5 @@ def post_report():
return jsonapi.JsonApiError(400, "Invalid report payload: " + str(exc)) return jsonapi.JsonApiError(400, "Invalid report payload: " + str(exc))
return { return {
"status": "ok" "data": r.to_json()
} }

View File

@ -13,6 +13,8 @@ export function saveReport(type, lat, lng) {
lng, lng,
}), }),
}) })
.then(response => response.json())
.then(response => response.data)
.catch(exc => console.error(`Unable to post report: ${exc}.`)); .catch(exc => console.error(`Unable to post report: ${exc}.`));
} }

View File

@ -11,8 +11,6 @@
</template> </template>
<script> <script>
import * as api from '@/api';
import GCUMIcon from '@/assets/gcum.svg'; import GCUMIcon from '@/assets/gcum.svg';
import ObstacleIcon from '@/assets/obstacle.svg'; import ObstacleIcon from '@/assets/obstacle.svg';
import PotHoleIcon from '@/assets/pothole.svg'; import PotHoleIcon from '@/assets/pothole.svg';
@ -60,7 +58,11 @@ export default {
}, },
methods: { methods: {
saveReport(type) { saveReport(type) {
return api.saveReport(type, this.lat, this.lng) return this.$store.dispatch('saveReport', {
type,
lat: this.lat,
lng: this.lng,
})
.then(() => { .then(() => {
this.isActive = !this.isActive; this.isActive = !this.isActive;
}); });

View File

@ -10,8 +10,9 @@ import 'leaflet/dist/leaflet.css';
import 'vuetify/dist/vuetify.min.css'; import 'vuetify/dist/vuetify.min.css';
import App from './App.vue'; import App from './App.vue';
import router from './router';
import i18n from './i18n'; import i18n from './i18n';
import router from './router';
import store from './store';
Vue.use(Vuetify); Vue.use(Vuetify);
@ -28,6 +29,7 @@ new Vue({
el: '#app', el: '#app',
router, router,
i18n, i18n,
store,
components: { App }, components: { App },
template: '<App/>', template: '<App/>',
}); });

13
src/store/actions.js Normal file
View File

@ -0,0 +1,13 @@
import * as api from '@/api';
import { PUSH_REPORT, STORE_REPORTS } from './mutations-types';
export function fetchReports({ commit }) {
return api.getReports().then(
reports => commit(STORE_REPORTS, { reports }),
);
}
export function saveReport({ commit }, { type, lat, lng }) {
return api.saveReport(type, lat, lng)
.then(report => commit(PUSH_REPORT, { report }));
}

13
src/store/index.js Normal file
View File

@ -0,0 +1,13 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import { initialState as state, mutations } from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
actions,
state,
mutations,
});

View File

@ -0,0 +1,2 @@
export const PUSH_REPORT = 'PUSH_REPORT';
export const STORE_REPORTS = 'STORE_REPORTS';

14
src/store/mutations.js Normal file
View File

@ -0,0 +1,14 @@
import * as types from './mutations-types';
export const initialState = {
reports: [],
};
export const mutations = {
[types.STORE_REPORTS](state, { reports }) {
state.reports = reports;
},
[types.PUSH_REPORT](state, { report }) {
state.reports.push(report);
},
};

View File

@ -30,7 +30,6 @@
<script> <script>
import NoSleep from 'nosleep.js'; import NoSleep from 'nosleep.js';
import * as api from '@/api';
import Map from '@/components/Map.vue'; import Map from '@/components/Map.vue';
import ReportDialog from '@/components/ReportDialog/index.vue'; import ReportDialog from '@/components/ReportDialog/index.vue';
import { distance, mockLocation } from '@/tools'; import { distance, mockLocation } from '@/tools';
@ -47,7 +46,7 @@ export default {
created() { created() {
this.initializePositionWatching(); this.initializePositionWatching();
this.setNoSleep(); this.setNoSleep();
this.fetchReports(); this.$store.dispatch('fetchReports');
}, },
beforeDestroy() { beforeDestroy() {
this.disableNoSleep(); this.disableNoSleep();
@ -55,10 +54,7 @@ export default {
}, },
computed: { computed: {
reportsMarkers() { reportsMarkers() {
if (!this.reports) { return this.$store.state.reports.map(report => ({
return [];
}
return this.reports.map(report => ({
id: report.id, id: report.id,
latLng: [report.attributes.lat, report.attributes.lng], latLng: [report.attributes.lat, report.attributes.lng],
})); }));
@ -72,7 +68,6 @@ export default {
lat: null, lat: null,
lng: null, lng: null,
noSleep: null, noSleep: null,
reports: null,
watchID: null, watchID: null,
}; };
}, },
@ -121,7 +116,7 @@ export default {
[position.coords.latitude, position.coords.longitude], [position.coords.latitude, position.coords.longitude],
); );
if (distanceFromPreviousPoint > UPDATE_REPORTS_DISTANCE_THRESHOLD) { if (distanceFromPreviousPoint > UPDATE_REPORTS_DISTANCE_THRESHOLD) {
this.fetchReports(); this.$store.dispatch('fetchReports');
} }
} }
this.lat = position.coords.latitude; this.lat = position.coords.latitude;
@ -141,11 +136,6 @@ export default {
this.noSleep.disable(); this.noSleep.disable();
} }
}, },
fetchReports() {
api.getReports().then((reports) => {
this.reports = reports;
});
},
}, },
}; };
</script> </script>

837
yarn.lock

File diff suppressed because it is too large Load Diff