Add an ICS feed of visits

UI is minimalist and should be improved in the future. Fixes #40.
This commit is contained in:
Lucas Verney 2017-11-09 17:33:39 +01:00
parent d6bee1dcb0
commit c936228726
12 changed files with 157 additions and 3 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ config/
node_modules node_modules
flatisfy/web/static/assets flatisfy/web/static/assets
data/ data/
package-lock.json

View File

@ -93,6 +93,9 @@ class Flat(BASE):
# Status # Status
status = Column(Enum(FlatStatus), default=FlatStatus.new) status = Column(Enum(FlatStatus), default=FlatStatus.new)
# Date for visit
visit_date = Column(DateTime)
@staticmethod @staticmethod
def from_dict(flat_dict): def from_dict(flat_dict):
""" """

View File

@ -83,6 +83,11 @@ def get_app(config):
api_routes.update_flat_notes_v1) api_routes.update_flat_notes_v1)
app.route("/api/v1/flat/:flat_id/notation", "POST", app.route("/api/v1/flat/:flat_id/notation", "POST",
api_routes.update_flat_notation_v1) api_routes.update_flat_notation_v1)
app.route("/api/v1/flat/:flat_id/visit_date", "POST",
api_routes.update_flat_visit_date_v1)
app.route("/api/v1/visits.ics", "GET",
api_routes.ics_feed_v1)
app.route("/api/v1/search", "POST", api_routes.search_v1) app.route("/api/v1/search", "POST", api_routes.search_v1)

View File

@ -6,7 +6,10 @@ require('isomorphic-fetch')
const postProcessAPIResults = function (flat) { const postProcessAPIResults = function (flat) {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
if (flat.date) { if (flat.date) {
flat.date = moment(flat.date) flat.date = moment.utc(flat.date)
}
if (flat.visit_date) {
flat.visit_date = moment.utc(flat.visit_date)
} }
if (flat.flatisfy_time_to) { if (flat.flatisfy_time_to) {
const momentifiedTimeTo = {} const momentifiedTimeTo = {}
@ -110,6 +113,24 @@ export const updateFlatNotation = function (flatId, newNotation, callback) {
}) })
} }
export const updateFlatVisitDate = function (flatId, newVisitDate, callback) {
fetch(
'/api/v1/flat/' + encodeURIComponent(flatId) + '/visit_date',
{
credentials: 'same-origin',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
visit_date: newVisitDate
})
}
).then(callback).catch(function (ex) {
console.error('Unable to update flat date of visit: ' + ex)
})
}
export const getTimeToPlaces = function (callback) { export const getTimeToPlaces = function (callback) {
fetch('/api/v1/time_to_places', { credentials: 'same-origin' }) fetch('/api/v1/time_to_places', { credentials: 'same-origin' })
.then(function (response) { .then(function (response) {

View File

@ -45,6 +45,8 @@ export default {
'Times_to': 'Times to', 'Times_to': 'Times to',
'Location': 'Location', 'Location': 'Location',
'Contact': 'Contact', 'Contact': 'Contact',
'Visit': 'Visit',
'setDateOfVisit': 'Set date of visit',
'no_phone_found': 'No phone found', 'no_phone_found': 'No phone found',
'rooms': 'room | rooms', 'rooms': 'room | rooms',
'bedrooms': 'bedroom | bedrooms' 'bedrooms': 'bedroom | bedrooms'

View File

@ -39,6 +39,12 @@ export default {
commit(types.UPDATE_FLAT_NOTES, { flatId, newNotes }) commit(types.UPDATE_FLAT_NOTES, { flatId, newNotes })
}) })
}, },
updateFlatVisitDate ({ commit }, { flatId, newVisitDate }) {
commit(types.IS_LOADING)
api.updateFlatVisitDate(flatId, newVisitDate, response => {
commit(types.UPDATE_FLAT_VISIT_DATE, { flatId, newVisitDate })
})
},
doSearch ({ commit }, { query }) { doSearch ({ commit }, { query }) {
commit(types.IS_LOADING) commit(types.IS_LOADING)
api.doSearch(query, flats => { api.doSearch(query, flats => {

View File

@ -3,5 +3,6 @@ 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 UPDATE_FLAT_NOTES = 'UPDATE_FLAT_NOTES'
export const UPDATE_FLAT_NOTATION = 'UPDATE_FLAT_NOTATION' export const UPDATE_FLAT_NOTATION = 'UPDATE_FLAT_NOTATION'
export const UPDATE_FLAT_VISIT_DATE = 'UPDATE_FLAT_VISIT_DATE'
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

@ -47,6 +47,13 @@ export const mutations = {
} }
state.loading -= 1 state.loading -= 1
}, },
[types.UPDATE_FLAT_VISIT_DATE] (state, { flatId, newVisitDate }) {
const index = state.flats.findIndex(flat => flat.id === flatId)
if (index > -1) {
Vue.set(state.flats[index], 'visit-date', newVisitDate)
}
state.loading -= 1
},
[types.RECEIVE_TIME_TO_PLACES] (state, { timeToPlaces }) { [types.RECEIVE_TIME_TO_PLACES] (state, { timeToPlaces }) {
state.timeToPlaces = timeToPlaces state.timeToPlaces = timeToPlaces
state.loading -= 1 state.loading -= 1

View File

@ -147,6 +147,15 @@
</p> </p>
</div> </div>
<h3>{{ $t("flatsDetails.Visit") }}</h3>
<div class="visit">
<flat-pickr
:value="flatpickrValue"
:config="flatpickrConfig"
:placeholder="$t('flatsDetails.setDateOfVisit')"
/>
</div>
<h3>{{ $t("common.Actions") }}</h3> <h3>{{ $t("common.Actions") }}</h3>
<nav> <nav>
@ -187,7 +196,10 @@
</template> </template>
<script> <script>
import flatPickr from 'vue-flatpickr-component'
import moment from 'moment'
import 'font-awesome-webpack' import 'font-awesome-webpack'
import 'flatpickr/dist/flatpickr.css'
import FlatsMap from '../components/flatsmap.vue' import FlatsMap from '../components/flatsmap.vue'
import Slider from '../components/slider.vue' import Slider from '../components/slider.vue'
@ -197,7 +209,8 @@ import { capitalize, range } from '../tools'
export default { export default {
components: { components: {
FlatsMap, FlatsMap,
Slider Slider,
flatPickr
}, },
created () { created () {
@ -220,7 +233,15 @@ export default {
data () { data () {
return { return {
'overloadNotation': null // TODO: Flatpickr locale
'overloadNotation': null,
'flatpickrConfig': {
static: true,
altFormat: 'h:i K, M j, Y',
altInput: true,
enableTime: true,
onChange: selectedDates => this.updateFlatVisitDate(selectedDates.length > 0 ? selectedDates[0] : null),
},
} }
}, },
@ -237,6 +258,12 @@ export default {
flat () { flat () {
return this.$store.getters.flat(this.$route.params.id) return this.$store.getters.flat(this.$route.params.id)
}, },
'flatpickrValue' () {
if (this.flat && this.flat.visit_date) {
return this.flat.visit_date.local().format()
}
return null
},
timeToPlaces () { timeToPlaces () {
return this.$store.getters.timeToPlaces(this.flat.flatisfy_constraint) return this.$store.getters.timeToPlaces(this.flat.flatisfy_constraint)
}, },
@ -304,6 +331,16 @@ export default {
) )
}, },
updateFlatVisitDate (date) {
if (date) {
date = moment(date).utc().format()
}
this.$store.dispatch(
'updateFlatVisitDate',
{ flatId: this.$route.params.id, newVisitDate: date }
)
},
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)

View File

@ -6,9 +6,12 @@ from __future__ import (
absolute_import, division, print_function, unicode_literals absolute_import, division, print_function, unicode_literals
) )
import datetime
import json import json
import arrow
import bottle import bottle
import vobject
import flatisfy.data import flatisfy.data
from flatisfy.models import flat as flat_model from flatisfy.models import flat as flat_model
@ -222,6 +225,36 @@ def update_flat_notation_v1(flat_id, db):
} }
def update_flat_visit_date_v1(flat_id, db):
"""
API v1 route to update flat date of visit:
POST /api/v1/flat/:flat_id/visit_date
Data: {
"visit_date": "ISO8601 DATETIME"
}
: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:
visit_date = json.load(bottle.request.body)["visit_date"]
if visit_date:
visit_date = arrow.get(visit_date).naive
flat.visit_date = visit_date
except (arrow.parser.ParserError, ValueError, KeyError):
return bottle.HTTPError(400, "Invalid visit date 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.
@ -290,3 +323,39 @@ def search_v1(db, config):
return { return {
"data": flats "data": flats
} }
def ics_feed_v1(config, db):
"""
API v1 ICS feed of visits route:
GET /api/v1/visits.ics
:return: The ICS feed for the visits.
"""
flats_with_visits = db.query(flat_model.Flat).filter(
flat_model.Flat.visit_date.isnot(None)
).all()
cal = vobject.iCalendar()
for flat in flats_with_visits:
vevent = cal.add('vevent')
vevent.add('dtstart').value = flat.visit_date
vevent.add('dtend').value = (
flat.visit_date + datetime.timedelta(hours=1)
)
vevent.add('summary').value = 'Visit - {}'.format(flat.title)
description = (
'{} (area: {}, cost: {} {})\n{}#/flat/{}\n'.format(
flat.title, flat.area, flat.cost, flat.currency,
config['website_url'], flat.id
)
)
description += '\n{}\n'.format(flat.text)
if flat.notes:
description += '\n{}\n'.format(flat.notes)
vevent.add('description').value = description
return cal.serialize()

View File

@ -27,6 +27,7 @@
"masonry": "0.0.2", "masonry": "0.0.2",
"moment": "^2.18.1", "moment": "^2.18.1",
"vue": "^2.2.6", "vue": "^2.2.6",
"vue-flatpickr-component": "^4.0.0",
"vue-i18n": "^6.1.1", "vue-i18n": "^6.1.1",
"vue-images-loaded": "^1.1.2", "vue-images-loaded": "^1.1.2",
"vue-router": "^2.4.0", "vue-router": "^2.4.0",

View File

@ -10,6 +10,7 @@ pillow
requests requests
sqlalchemy sqlalchemy
unidecode unidecode
vobject
whoosh whoosh
https://git.weboob.org/weboob/devel/repository/archive.zip?ref=master https://git.weboob.org/weboob/devel/repository/archive.zip?ref=master
https://git.weboob.org/weboob/modules/repository/archive.zip?ref=master https://git.weboob.org/weboob/modules/repository/archive.zip?ref=master