Add a page listing flats by status

Also display the journeys on the details view.
This commit is contained in:
Lucas Verney 2017-04-27 21:02:17 +02:00
parent 5f2f4d0ccf
commit 0fb5f28184
8 changed files with 198 additions and 20 deletions

View File

@ -379,7 +379,7 @@ def compute_travel_times(flats_list, config):
if time_to_place: if time_to_place:
LOGGER.info( LOGGER.info(
"Travel time between %s and flat %s is %ds.", "Travel time between %s and flat %s is %ds.",
place_name, flat["id"], time_to_place place_name, flat["id"], time_to_place["time"]
) )
flat["flatisfy"]["time_to"][place_name] = time_to_place flat["flatisfy"]["time_to"][place_name] = time_to_place
return flats_list return flats_list

View File

@ -235,8 +235,8 @@ def get_travel_time_between(latlng_from, latlng_to, config):
:param latlng_from: A tuple of (latitude, longitude) for the starting :param latlng_from: A tuple of (latitude, longitude) for the starting
point. point.
:param latlng_to: A tuple of (latitude, longitude) for the destination. :param latlng_to: A tuple of (latitude, longitude) for the destination.
:return: The travel time in seconds. Returns ``None`` if it could not fetch :return: A dict of the travel time in seconds and sections of the journey
it. with GeoJSON paths. Returns ``None`` if it could not fetch it.
.. note :: Uses the Navitia API. Requires a ``navitia_api_key`` field to be .. note :: Uses the Navitia API. Requires a ``navitia_api_key`` field to be
filled-in in the ``config``. filled-in in the ``config``.
@ -258,7 +258,28 @@ def get_travel_time_between(latlng_from, latlng_to, config):
auth=(config["navitia_api_key"], "") auth=(config["navitia_api_key"], "")
) )
req.raise_for_status() req.raise_for_status()
time = req.json()["journeys"][0]["durations"]["total"]
journeys = req.json()["journeys"][0]
time = journeys["durations"]["total"]
sections = []
for section in journeys["sections"]:
if section["type"] == "public_transport":
# Public transport
sections.append({
"geojson": section["geojson"],
"color": (
section["display_informations"].get("color", None)
)
})
elif section["type"] == "street_network":
# Walking
sections.append({
"geojson": section["geojson"],
"color": None
})
else:
# Skip anything else
continue
except (requests.exceptions.RequestException, except (requests.exceptions.RequestException,
ValueError, IndexError, KeyError) as exc: ValueError, IndexError, KeyError) as exc:
# Ignore any possible exception # Ignore any possible exception
@ -272,4 +293,10 @@ def get_travel_time_between(latlng_from, latlng_to, config):
"No API key available for travel time lookup. Please provide " "No API key available for travel time lookup. Please provide "
"a Navitia API key. Skipping travel time lookup." "a Navitia API key. Skipping travel time lookup."
) )
return time if time:
return {
"time": time,
"sections": sections
}
else:
return None

View File

@ -4,9 +4,8 @@
<nav> <nav>
<ul> <ul>
<li><router-link :to="{name: 'home'}">{{ $t("menu.available_flats") }}</router-link></li> <li><router-link :to="{name: 'home'}">{{ $t("menu.available_flats") }}</router-link></li>
<li><router-link :to="{name: 'followed'}">{{ $t("menu.followed_flats") }}</router-link></li> <li><router-link :to="{name: 'status', params: {status: 'followed'}}">{{ $t("menu.followed_flats") }}</router-link></li>
<li><router-link :to="{name: 'ignored'}">{{ $t("menu.ignored_flats") }}</router-link></li> <li><router-link :to="{name: 'status', params: {status: 'new'}}">{{ $t("menu.by_status") }}</router-link></li>
<li><router-link :to="{name: 'user_deleted'}">{{ $t("menu.user_deleted_flats") }}</router-link></li>
</ul> </ul>
</nav> </nav>
<router-view></router-view> <router-view></router-view>

View File

@ -12,6 +12,9 @@
<v-tooltip :content="place_name"></v-tooltip> <v-tooltip :content="place_name"></v-tooltip>
</v-marker> </v-marker>
</template> </template>
<template v-for="journey in journeys">
<v-geojson-layer :geojson="journey.geojson" :options="Object.assign({}, defaultGeoJSONOptions, journey.options)"></v-geojson-layer>
</template>
</v-map> </v-map>
</div> </div>
</template> </template>
@ -30,6 +33,13 @@ import Vue2Leaflet from 'vue2-leaflet'
export default { export default {
data () { data () {
return { return {
defaultGeoJSONOptions: {
weight: 5,
color: '#000',
opacity: 1,
fillColor: '#e4ce7f',
fillOpacity: 1
},
center: null, center: null,
zoom: { zoom: {
defaultZoom: 13, defaultZoom: 13,
@ -59,7 +69,8 @@ export default {
'v-tilelayer': Vue2Leaflet.TileLayer, 'v-tilelayer': Vue2Leaflet.TileLayer,
'v-marker': Vue2Leaflet.Marker, 'v-marker': Vue2Leaflet.Marker,
'v-tooltip': Vue2Leaflet.Tooltip, 'v-tooltip': Vue2Leaflet.Tooltip,
'v-popup': Vue2Leaflet.Popup 'v-popup': Vue2Leaflet.Popup,
'v-geojson-layer': Vue2Leaflet.GeoJSON
}, },
computed: { computed: {
@ -77,7 +88,7 @@ export default {
} }
}, },
props: ['flats', 'places'] props: ['flats', 'places', 'journeys']
// TODO: Add a switch to display a layer with isochrones // TODO: Add a switch to display a layer with isochrones
} }

View File

@ -21,8 +21,7 @@ export default {
menu: { menu: {
'available_flats': 'Available flats', 'available_flats': 'Available flats',
'followed_flats': 'Followed flats', 'followed_flats': 'Followed flats',
'ignored_flats': 'Ignored flats', 'by_status': 'Flats by status'
'user_deleted_flats': 'User deleted flats'
}, },
flatsDetails: { flatsDetails: {
'Title': 'Title', 'Title': 'Title',
@ -49,7 +48,8 @@ export default {
'new': 'new', 'new': 'new',
'followed': 'followed', 'followed': 'followed',
'ignored': 'ignored', 'ignored': 'ignored',
'user_deleted': 'user deleted' 'user_deleted': 'user deleted',
'duplicate': 'duplicate'
}, },
slider: { slider: {
'Fullscreen_photo': 'Fullscreen photo' 'Fullscreen_photo': 'Fullscreen photo'

View File

@ -11,9 +11,7 @@ export default new VueRouter({
routes: [ routes: [
{ path: '/', component: Home, name: 'home' }, { path: '/', component: Home, name: 'home' },
{ path: '/new', redirect: '/' }, { path: '/new', redirect: '/' },
{ path: '/followed', component: Status, name: 'followed' }, { path: '/status/:status', component: Status, name: 'status' },
{ path: '/ignored', component: Status, name: 'ignored' },
{ path: '/user_deleted', component: Status, name: 'user_deleted' },
{ path: '/flat/:id', component: Details, name: 'details' } { path: '/flat/:id', component: Details, name: 'details' }
] ]
}) })

View File

@ -84,7 +84,7 @@
<template v-if="Object.keys(flat.flatisfy_time_to).length"> <template v-if="Object.keys(flat.flatisfy_time_to).length">
<ul class="time_to_list"> <ul class="time_to_list">
<li v-for="(time_to, place) in flat.flatisfy_time_to" :key="place"> <li v-for="(time_to, place) in flat.flatisfy_time_to" :key="place">
{{ place }}: {{ time_to }} {{ place }}: {{ time_to["time"] }}
</li> </li>
</ul> </ul>
</template> </template>
@ -98,7 +98,7 @@
<div> <div>
<h3>{{ $t("flatsDetails.Location") }}</h3> <h3>{{ $t("flatsDetails.Location") }}</h3>
<FlatsMap :flats="flatMarkers" :places="timeToPlaces"></FlatsMap> <FlatsMap :flats="flatMarkers" :places="timeToPlaces" :journeys="journeys"></FlatsMap>
</div> </div>
</div> </div>
<div class="right-panel"> <div class="right-panel">
@ -195,6 +195,24 @@ export default {
flat () { flat () {
return this.$store.getters.flat(this.$route.params.id) return this.$store.getters.flat(this.$route.params.id)
}, },
journeys () {
if (Object.keys(this.flat.flatisfy_time_to).length > 0) {
const journeys = []
for (const place in this.flat.flatisfy_time_to) {
this.flat.flatisfy_time_to[place].sections.forEach(
section => journeys.push({
geojson: section.geojson,
options: {
color: section.color ? ('#' + section.color) : '#2196f3',
dashArray: section.color ? 'none' : '2, 10'
}
})
)
}
return journeys
}
return []
},
displayedStations () { displayedStations () {
if (this.flat.flatisfy_stations.length > 0) { if (this.flat.flatisfy_stations.length > 0) {
const stationsNames = this.flat.flatisfy_stations.map(station => station.name) const stationsNames = this.flat.flatisfy_stations.map(station => station.name)

View File

@ -1,6 +1,18 @@
<template> <template>
<div> <div>
<h2>{{ capitalize($t("status." + $route.name)) }}</h2> <h2 class="btn-group">
<button type="button" class="dropdownToggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-on:click="toggleDropdown()" ref="dropdownToggle">
<span class="dashedUnderline">{{ capitalize($t("status." + $route.params.status)) }}</span>
<i class="fa fa-caret-down"/>
</button>
<ul class="dropdownMenu" :class="isDropdownVisible ? '' : 'hidden'">
<li v-for="status in available_status" :key="status" :class="$route.params.status == status.toLowerCase() ? 'active' : ''">
<router-link :to="{ name: 'status', params: {status: status.toLowerCase()}}" role="button">
{{ capitalize($t("status." + status)) }}
</router-link>
</li>
</ul>
</h2>
<template v-if="Object.keys(postalCodesFlatsBuckets).length"> <template v-if="Object.keys(postalCodesFlatsBuckets).length">
<template v-for="(postal_code_data, postal_code) in postalCodesFlatsBuckets"> <template v-for="(postal_code_data, postal_code) in postalCodesFlatsBuckets">
<h3>{{ postal_code_data.name }} ({{ postal_code }}) - {{ postal_code_data.flats.length }} {{ $tc("common.flats", 42) }}</h3> <h3>{{ postal_code_data.name }} ({{ postal_code }}) - {{ postal_code_data.flats.length }} {{ $tc("common.flats", 42) }}</h3>
@ -19,6 +31,19 @@ import { capitalize } from '../tools'
import FlatsTable from '../components/flatstable.vue' import FlatsTable from '../components/flatstable.vue'
export default { export default {
data () {
return {
'isDropdownVisible': false,
'available_status': [
'new',
'followed',
'ignored',
'duplicate',
'user_deleted'
]
}
},
components: { components: {
FlatsTable FlatsTable
}, },
@ -28,14 +53,114 @@ export default {
this.$store.dispatch('getAllFlats') this.$store.dispatch('getAllFlats')
}, },
mounted () {
window.addEventListener('click', event => {
if (event.target !== this.$refs.dropdownToggle) {
this.isDropdownVisible = false
}
})
},
computed: { computed: {
postalCodesFlatsBuckets () { postalCodesFlatsBuckets () {
return this.$store.getters.postalCodesFlatsBuckets(flat => flat.status === this.$route.name) return this.$store.getters.postalCodesFlatsBuckets(flat => flat.status === this.$route.params.status)
} }
}, },
methods: { methods: {
toggleDropdown () {
this.isDropdownVisible = !this.isDropdownVisible
},
capitalize: capitalize capitalize: capitalize
} }
} }
</script> </script>
<style scoped>
.btn-group {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.btn-group > .btn {
position: relative;
flex: 0 1 auto;
margin-bottom: 0;
}
.btn-group > .btn:hover {
z-index: 2;
}
.btn-group > .btn:focus,
.btn-group > .btn:active,
.btn-group > .btn.active {
z-index: 2;
}
.dropdownToggle {
border: none;
padding: 0;
line-height: 1em;
background-color: transparent;
font-size: 21px;
font-weight: 700;
color: #333;
font-family: "Helvetica", "Arial", sans-serif;
}
.hidden {
display: none;
}
.dropdownMenu {
position: absolute;
top: 10px;
min-width: 160px;
background-color: white;
z-index: 1;
padding-left: 0;
border-radius: .25rem;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
.dropdownMenu li {
list-style-type: none;
padding-left: 1em;
border-radius: .25rem;
}
.dropdownMenu a {
text-decoration: none;
color: #555;
font-size: 0.75em;
font-weight: normal;
}
.dashedUnderline {
border-bottom: 1px dotted black;
}
.active {
background-color: #0275d8;
}
.dropdownMenu li.active a {
color: white;
}
.fa-caret-down {
display: inline-block;
font-size: 15px;
margin-top: 1em;
margin-left: 0.1em;
}
.dashedUnderline,
.fa-caret-down {
/* Fix for alignment of caret and border-bottom */
display: inline-block;
float: left;
}
</style>