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
|
||||
flatisfy/web/static/assets
|
||||
data/
|
||||
package-lock.json
|
||||
|
@ -93,6 +93,9 @@ class Flat(BASE):
|
||||
# Status
|
||||
status = Column(Enum(FlatStatus), default=FlatStatus.new)
|
||||
|
||||
# Date for visit
|
||||
visit_date = Column(DateTime)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(flat_dict):
|
||||
"""
|
||||
|
@ -83,6 +83,11 @@ def get_app(config):
|
||||
api_routes.update_flat_notes_v1)
|
||||
app.route("/api/v1/flat/:flat_id/notation", "POST",
|
||||
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)
|
||||
|
||||
|
@ -6,7 +6,10 @@ require('isomorphic-fetch')
|
||||
const postProcessAPIResults = function (flat) {
|
||||
/* eslint-disable camelcase */
|
||||
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) {
|
||||
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) {
|
||||
fetch('/api/v1/time_to_places', { credentials: 'same-origin' })
|
||||
.then(function (response) {
|
||||
|
@ -45,6 +45,8 @@ export default {
|
||||
'Times_to': 'Times to',
|
||||
'Location': 'Location',
|
||||
'Contact': 'Contact',
|
||||
'Visit': 'Visit',
|
||||
'setDateOfVisit': 'Set date of visit',
|
||||
'no_phone_found': 'No phone found',
|
||||
'rooms': 'room | rooms',
|
||||
'bedrooms': 'bedroom | bedrooms'
|
||||
|
@ -39,6 +39,12 @@ export default {
|
||||
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 }) {
|
||||
commit(types.IS_LOADING)
|
||||
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_NOTES = 'UPDATE_FLAT_NOTES'
|
||||
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 IS_LOADING = 'IS_LOADING'
|
||||
|
@ -47,6 +47,13 @@ export const mutations = {
|
||||
}
|
||||
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 }) {
|
||||
state.timeToPlaces = timeToPlaces
|
||||
state.loading -= 1
|
||||
|
@ -147,6 +147,15 @@
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<nav>
|
||||
@ -187,7 +196,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import moment from 'moment'
|
||||
import 'font-awesome-webpack'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
import FlatsMap from '../components/flatsmap.vue'
|
||||
import Slider from '../components/slider.vue'
|
||||
@ -197,7 +209,8 @@ import { capitalize, range } from '../tools'
|
||||
export default {
|
||||
components: {
|
||||
FlatsMap,
|
||||
Slider
|
||||
Slider,
|
||||
flatPickr
|
||||
},
|
||||
|
||||
created () {
|
||||
@ -220,7 +233,15 @@ export default {
|
||||
|
||||
data () {
|
||||
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 () {
|
||||
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 () {
|
||||
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) {
|
||||
const minutes = Math.floor(time.as('minutes'))
|
||||
return minutes + ' ' + this.$tc('common.mins', minutes)
|
||||
|
@ -6,9 +6,12 @@ from __future__ import (
|
||||
absolute_import, division, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import arrow
|
||||
import bottle
|
||||
import vobject
|
||||
|
||||
import flatisfy.data
|
||||
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):
|
||||
"""
|
||||
API v1 route to fetch the details of the places to compute time to.
|
||||
@ -290,3 +323,39 @@ def search_v1(db, config):
|
||||
return {
|
||||
"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",
|
||||
"moment": "^2.18.1",
|
||||
"vue": "^2.2.6",
|
||||
"vue-flatpickr-component": "^4.0.0",
|
||||
"vue-i18n": "^6.1.1",
|
||||
"vue-images-loaded": "^1.1.2",
|
||||
"vue-router": "^2.4.0",
|
||||
|
@ -10,6 +10,7 @@ pillow
|
||||
requests
|
||||
sqlalchemy
|
||||
unidecode
|
||||
vobject
|
||||
whoosh
|
||||
https://git.weboob.org/weboob/devel/repository/archive.zip?ref=master
|
||||
https://git.weboob.org/weboob/modules/repository/archive.zip?ref=master
|
||||
|
Loading…
Reference in New Issue
Block a user