Add the possibility to leave notes on flats

* Add a database field to store `notes` (as a memo) about flats.
* Add matching UI elements to let users store their notes about flats.

This commit closes issue #34.
This commit is contained in:
Lucas Verney 2017-05-03 19:17:19 +02:00
parent 8a50dd3302
commit 69588a9601
No known key found for this signature in database
GPG Key ID: 75B45CF41F334690
10 changed files with 89 additions and 9 deletions

View File

@ -12,6 +12,7 @@ Based on Flask-whooshalchemy by Karl Gyllstrom (Flask is still supported, but no
:copyright: (c) 2012 by Karl Gyllstrom :copyright: (c) 2012 by Karl Gyllstrom
:license: BSD (see LICENSE.txt) :license: BSD (see LICENSE.txt)
""" """
# pylint: skip-file
from __future__ import absolute_import, print_function, unicode_literals from __future__ import absolute_import, print_function, unicode_literals

View File

@ -76,6 +76,7 @@ class Flat(BASE):
title = Column(String) title = Column(String)
urls = Column(MagicJSON) urls = Column(MagicJSON)
merged_ids = Column(MagicJSON) merged_ids = Column(MagicJSON)
notes = Column(Text)
# Flatisfy data # Flatisfy data
# TODO: Should be in another table with relationships # TODO: Should be in another table with relationships

View File

@ -78,6 +78,8 @@ def get_app(config):
app.route("/api/v1/flat/:flat_id", "GET", api_routes.flat_v1) app.route("/api/v1/flat/:flat_id", "GET", api_routes.flat_v1)
app.route("/api/v1/flat/:flat_id/status", "POST", app.route("/api/v1/flat/:flat_id/status", "POST",
api_routes.update_flat_status_v1) api_routes.update_flat_status_v1)
app.route("/api/v1/flat/:flat_id/notes", "POST",
api_routes.update_flat_notes_v1)
app.route("/api/v1/search", "POST", api_routes.search_v1) app.route("/api/v1/search", "POST", api_routes.search_v1)

View File

@ -68,7 +68,24 @@ export const updateFlatStatus = function (flatId, newStatus, callback) {
).then(callback).catch(function (ex) { ).then(callback).catch(function (ex) {
console.error('Unable to update flat status: ' + ex) console.error('Unable to update flat status: ' + ex)
}) })
}
export const updateFlatNotes = function (flatId, newNotes, callback) {
fetch(
'/api/v1/flat/' + encodeURIComponent(flatId) + '/notes',
{
credentials: 'same-origin',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
notes: newNotes
})
}
).then(callback).catch(function (ex) {
console.error('Unable to update flat notes: ' + ex)
})
} }
export const getTimeToPlaces = function (callback) { export const getTimeToPlaces = function (callback) {
@ -82,7 +99,6 @@ export const getTimeToPlaces = function (callback) {
}) })
} }
export const doSearch = function (query, callback) { export const doSearch = function (query, callback) {
fetch( fetch(
'/api/v1/search', '/api/v1/search',
@ -101,5 +117,4 @@ export const doSearch = function (query, callback) {
}).catch(function (ex) { }).catch(function (ex) {
console.error('Unable to perform search: ' + ex) console.error('Unable to perform search: ' + ex)
}) })
} }

View File

@ -27,6 +27,12 @@ export default {
commit(types.UPDATE_FLAT_STATUS, { flatId, newStatus }) commit(types.UPDATE_FLAT_STATUS, { flatId, newStatus })
}) })
}, },
updateFlatNotes ({ commit }, { flatId, newNotes }) {
commit(types.IS_LOADING)
api.updateFlatNotes(flatId, newNotes, response => {
commit(types.UPDATE_FLAT_NOTES, { flatId, newNotes })
})
},
doSearch ({ commit }, { query }) { doSearch ({ commit }, { query }) {
commit(types.IS_LOADING) commit(types.IS_LOADING)
api.doSearch(query, flats => { api.doSearch(query, flats => {

View File

@ -1,5 +1,6 @@
export const REPLACE_FLATS = 'REPLACE_FLATS' export const REPLACE_FLATS = 'REPLACE_FLATS'
export const MERGE_FLATS = 'MERGE_FLATS' export const MERGE_FLATS = 'MERGE_FLATS'
export const UPDATE_FLAT_STATUS = 'UPDATE_FLAT_STATUS' export const UPDATE_FLAT_STATUS = 'UPDATE_FLAT_STATUS'
export const UPDATE_FLAT_NOTES = 'UPDATE_FLAT_NOTES'
export const RECEIVE_TIME_TO_PLACES = 'RECEIVE_TIME_TO_PLACES' export const RECEIVE_TIME_TO_PLACES = 'RECEIVE_TIME_TO_PLACES'
export const IS_LOADING = 'IS_LOADING' export const IS_LOADING = 'IS_LOADING'

View File

@ -32,6 +32,13 @@ export const mutations = {
Vue.set(state.flats[index], 'status', newStatus) Vue.set(state.flats[index], 'status', newStatus)
} }
}, },
[types.UPDATE_FLAT_NOTES] (state, { flatId, newNotes }) {
state.loading = false
const index = state.flats.findIndex(flat => flat.id === flatId)
if (index > -1) {
Vue.set(state.flats[index], 'notes', newNotes)
}
},
[types.RECEIVE_TIME_TO_PLACES] (state, { timeToPlaces }) { [types.RECEIVE_TIME_TO_PLACES] (state, { timeToPlaces }) {
state.loading = false state.loading = false
state.timeToPlaces = timeToPlaces state.timeToPlaces = timeToPlaces

View File

@ -101,6 +101,14 @@
<FlatsMap :flats="flatMarkers" :places="timeToPlaces" :journeys="journeys"></FlatsMap> <FlatsMap :flats="flatMarkers" :places="timeToPlaces" :journeys="journeys"></FlatsMap>
</div> </div>
<div>
<h3>Notes</h3>
<form v-on:submit="updateFlatNotes">
<textarea ref="notesTextarea" rows="10">{{ flat.notes }}</textarea>
<p class="right"><input type="submit" value="Save"/></p>
</form>
</div>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<h3>{{ $t("flatsDetails.Contact") }}</h3> <h3>{{ $t("flatsDetails.Contact") }}</h3>
@ -242,6 +250,14 @@ export default {
this.$store.dispatch('updateFlatStatus', { flatId: this.$route.params.id, newStatus: status }) this.$store.dispatch('updateFlatStatus', { flatId: this.$route.params.id, newStatus: status })
}, },
updateFlatNotes () {
const notes = this.$refs.notesTextarea.value
this.$store.dispatch(
'updateFlatNotes',
{ flatId: this.$route.params.id, newNotes: notes }
)
},
humanizeTimeTo (time) { humanizeTimeTo (time) {
const minutes = Math.floor(time.as('minutes')) const minutes = Math.floor(time.as('minutes'))
return minutes + ' ' + this.$tc('common.mins', minutes) return minutes + ' ' + this.$tc('common.mins', minutes)
@ -269,6 +285,10 @@ export default {
grid-row: 1; grid-row: 1;
} }
.left-panel textarea {
width: 100%;
}
.right { .right {
text-align: right; text-align: right;
} }

View File

@ -47,11 +47,11 @@ export default {
} }
return this.$store.getters.postalCodesFlatsBuckets( return this.$store.getters.postalCodesFlatsBuckets(
flat => flat.status != "duplicate" && flat.status != "ignored" flat => flat.status !== 'duplicate' && flat.status !== 'ignored'
) )
}, },
loading () { loading () {
return this.$store.getters.isLoading; return this.$store.getters.isLoading
} }
}, },
@ -59,12 +59,12 @@ export default {
onSearch (event) { onSearch (event) {
event.preventDefault() event.preventDefault()
let query = this.$refs.searchInput.value const query = this.$refs.searchInput.value
this.$router.replace({ name: 'search', query: { query: query }}) this.$router.replace({ name: 'search', query: { query: query }})
}, },
doSearch () { doSearch () {
let query = this.$route.query.query const query = this.$route.query.query
if (query) { if (query) {
this.$store.dispatch('doSearch', { query: query }) this.$store.dispatch('doSearch', { query: query })

View File

@ -147,6 +147,33 @@ def update_flat_status_v1(flat_id, db):
} }
def update_flat_notes_v1(flat_id, db):
"""
API v1 route to update flat notes:
POST /api/v1/flat/:flat_id/notes
Data: {
"notes": "NEW_NOTES"
}
:return: The new flat object in a JSON ``data`` dict.
"""
flat = db.query(flat_model.Flat).filter_by(id=flat_id).first()
if not flat:
return bottle.HTTPError(404, "No flat with id {}.".format(flat_id))
try:
flat.notes = json.load(bottle.request.body)["notes"]
except (ValueError, KeyError):
return bottle.HTTPError(400, "Invalid notes provided.")
json_flat = flat.json_api_repr()
return {
"data": json_flat
}
def time_to_places_v1(config): def time_to_places_v1(config):
""" """
API v1 route to fetch the details of the places to compute time to. API v1 route to fetch the details of the places to compute time to.
@ -165,7 +192,7 @@ def time_to_places_v1(config):
} }
def search_v1(config, db): def search_v1(config):
""" """
API v1 route to perform a fulltext search on flats. API v1 route to perform a fulltext search on flats.