Add an ICS feed of visits
UI is minimalist and should be improved in the future. Fixes #40.
This commit is contained in:
parent
d6bee1dcb0
commit
c936228726
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ config/
|
|||||||
node_modules
|
node_modules
|
||||||
flatisfy/web/static/assets
|
flatisfy/web/static/assets
|
||||||
data/
|
data/
|
||||||
|
package-lock.json
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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'
|
||||||
|
@ -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 => {
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user