Browse Source
* Init Webpack / Babel / etc setup. * Build the app using Vue, Vue-router, Vuex. * i18n Some backends changes were made to match the webapp development: * Return the flat status as a single string ("new" rather than "FlatStatus.new") * Completely switch to calling Weboob API directly for fetching * Use Canister for Bottle logging * Handle merging of details dict better * Add a WSGI script * Keep track of duplicates * Webserver had to be restarted to fetch external changes to the db * Handle leboncoin module better Also add contributions guidelines. Closes issue #3 Closes issue #14.responsive
49 changed files with 1988 additions and 120 deletions
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
{ |
||||
"presets": ["es2015", "stage-0"], |
||||
"plugins": ["transform-runtime"] |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
{ |
||||
extends: ["vue", /* your other extends */], |
||||
plugins: ["vue"], |
||||
"env": { |
||||
"browser": true |
||||
}, |
||||
rules: { |
||||
'indent': ["error", 4, { 'SwitchCase': 1 }], |
||||
} |
||||
} |
@ -1,6 +1,8 @@
@@ -1,6 +1,8 @@
|
||||
build |
||||
*.json |
||||
*.pyc |
||||
*.swp |
||||
*.swo |
||||
*.db |
||||
config/ |
||||
node_modules |
||||
flatisfy/web/static/js |
||||
|
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
## TL;DR |
||||
|
||||
* Branch off `master`. |
||||
* One feature per commit. |
||||
* In case of changes request, amend your commit. |
||||
|
||||
|
||||
## Useful infos |
||||
|
||||
* There is a `hooks/pre-commit` file which can be used as a `pre-commit` git |
||||
hook to check coding style. |
||||
* Python coding style is PEP8. JS coding style is enforced by `eslint`. |
||||
* Some useful `npm` scripts are provided (`build` / `watch` / `lint`) |
||||
|
||||
|
||||
## Translating the webapp |
||||
|
||||
If you want to translate the webapp, just create a new folder in |
||||
`flatisfy/web/js_src/i18n` with the short name of your locale (typically, `en` |
||||
is for english). Copy the `flatisfy/web/js_src/i18n/en/index.js` file to this |
||||
new folder and translate the `messages` strings. |
||||
|
||||
Then, edit `flatisfy/web/js_src/i18n/index.js` file to include your new |
||||
locale. |
||||
|
||||
|
||||
## How to contribute |
||||
|
||||
* If you're thinking about a new feature, see if there's already an issue open |
||||
about it, or please open one otherwise. This will ensure that everybody is on |
||||
track for the feature and willing to see it in Flatisfy. |
||||
* One commit per feature. |
||||
* Branch off the `master ` branch. |
||||
* Check the linting of your code before doing a PR. |
||||
* Ideally, your merge-request should be mergeable without any merge commit, that |
||||
is, it should be a fast-forward merge. For this to happen, your code needs to |
||||
be always rebased onto `master`. Again, this is something nice to have that |
||||
I expect from recurring contributors, but not a big deal if you don't do it |
||||
otherwise. |
||||
* I'll look at it and might ask for a few changes. In this case, please create |
||||
new commits. When the final result looks good, I may ask you to squash the |
||||
WIP commits into a single one, to maintain the invariant of "one feature, one |
||||
commit". |
||||
|
||||
|
||||
Thanks! |
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
# coding: utf-8 |
||||
""" |
||||
This module contains a Bottle plugin to pass the config argument to any route |
||||
which needs it. |
||||
|
||||
This module is heavily based on code from |
||||
[Bottle-SQLAlchemy](https://github.com/iurisilvio/bottle-sqlalchemy) which is |
||||
licensed under MIT license. |
||||
""" |
||||
from __future__ import ( |
||||
absolute_import, division, print_function, unicode_literals |
||||
) |
||||
|
||||
import functools |
||||
import inspect |
||||
|
||||
import bottle |
||||
|
||||
|
||||
class ConfigPlugin(object): |
||||
""" |
||||
A Bottle plugin to automatically pass the config object to the routes |
||||
specifying they need it. |
||||
""" |
||||
name = 'config' |
||||
api = 2 |
||||
KEYWORD = "config" |
||||
|
||||
def __init__(self, config): |
||||
""" |
||||
:param config: The config object to pass. |
||||
""" |
||||
self.config = config |
||||
|
||||
def setup(self, app): # pylint: disable=no-self-use |
||||
""" |
||||
Make sure that other installed plugins don't affect the same |
||||
keyword argument and check if metadata is available. |
||||
""" |
||||
for other in app.plugins: |
||||
if not isinstance(other, ConfigPlugin): |
||||
continue |
||||
else: |
||||
raise bottle.PluginError( |
||||
"Found another conflicting Config plugin." |
||||
) |
||||
|
||||
def apply(self, callback, route): |
||||
""" |
||||
Method called on route invocation. Should apply some transformations to |
||||
the route prior to returing it. |
||||
|
||||
We check the presence of ``self.KEYWORD`` in the route signature and |
||||
replace the route callback by a partial invocation where we replaced |
||||
this argument by a valid config object. |
||||
""" |
||||
# Check whether the route needs a valid db session or not. |
||||
try: |
||||
callback_args = inspect.signature(route.callback).parameters |
||||
except AttributeError: |
||||
# inspect.signature does not exist on older Python |
||||
callback_args = inspect.getargspec(route.callback).args |
||||
|
||||
if self.KEYWORD not in callback_args: |
||||
# If no need for a db session, call the route callback |
||||
return callback |
||||
kwargs = {} |
||||
kwargs[self.KEYWORD] = self.config |
||||
return functools.partial(callback, **kwargs) |
||||
|
||||
|
||||
Plugin = ConfigPlugin |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
import moment from 'moment' |
||||
|
||||
require('es6-promise').polyfill() |
||||
require('isomorphic-fetch') |
||||
|
||||
export const getFlats = function (callback) { |
||||
fetch('/api/v1/flats') |
||||
.then(function (response) { |
||||
return response.json() |
||||
}).then(function (json) { |
||||
const flats = json.data |
||||
flats.map(flat => { |
||||
if (flat.date) { |
||||
flat.date = moment(flat.date) |
||||
} |
||||
return flat |
||||
}) |
||||
callback(flats) |
||||
}).catch(function (ex) { |
||||
console.error('Unable to parse flats: ' + ex) |
||||
}) |
||||
} |
||||
|
||||
export const getFlat = function (flatId, callback) { |
||||
fetch('/api/v1/flat/' + encodeURIComponent(flatId)) |
||||
.then(function (response) { |
||||
return response.json() |
||||
}).then(function (json) { |
||||
const flat = json.data |
||||
if (flat.date) { |
||||
flat.date = moment(flat.date) |
||||
} |
||||
callback(json.data) |
||||
}).catch(function (ex) { |
||||
console.error('Unable to parse flats: ' + ex) |
||||
}) |
||||
} |
||||
|
||||
export const updateFlatStatus = function (flatId, newStatus, callback) { |
||||
fetch( |
||||
'/api/v1/flat/' + encodeURIComponent(flatId) + '/status', |
||||
{ |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json' |
||||
}, |
||||
body: JSON.stringify({ |
||||
status: newStatus |
||||
}) |
||||
} |
||||
).then(callback) |
||||
} |
||||
|
||||
export const getTimeToPlaces = function (callback) { |
||||
fetch('/api/v1/time_to/places') |
||||
.then(function (response) { |
||||
return response.json() |
||||
}).then(function (json) { |
||||
callback(json.data) |
||||
}).catch(function (ex) { |
||||
console.error('Unable to fetch time to places: ' + ex) |
||||
}) |
||||
} |
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
<template> |
||||
<div> |
||||
<h1><router-link :to="{name: 'home'}">Flatisfy</router-link></h1> |
||||
<nav> |
||||
<ul> |
||||
<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: 'ignored'}">{{ $t("menu.ignored_flats") }}</router-link></li> |
||||
<li><router-link :to="{name: 'user_deleted'}">{{ $t("menu.user_deleted_flats") }}</router-link></li> |
||||
</ul> |
||||
</nav> |
||||
<router-view></router-view> |
||||
</div> |
||||
</template> |
||||
|
||||
<style> |
||||
body { |
||||
margin: 0 auto; |
||||
max-width: 75em; |
||||
font-family: "Helvetica", "Arial", sans-serif; |
||||
line-height: 1.5; |
||||
padding: 4em 1em; |
||||
padding-top: 1em; |
||||
color: #555; |
||||
} |
||||
|
||||
h1 { |
||||
text-align: center; |
||||
} |
||||
|
||||
h1, |
||||
h2, |
||||
strong, |
||||
th { |
||||
color: #333; |
||||
} |
||||
|
||||
table { |
||||
border-collapse: collapse; |
||||
margin: 1em; |
||||
width: calc(100% - 2em); |
||||
text-align: center; |
||||
} |
||||
|
||||
th, td { |
||||
padding: 1em; |
||||
border: 1px solid black; |
||||
} |
||||
|
||||
tbody>tr:hover { |
||||
background-color: #DDD; |
||||
} |
||||
</style> |
||||
|
||||
<style scoped> |
||||
h1 a { |
||||
color: inherit; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
nav { |
||||
text-align: center; |
||||
} |
||||
|
||||
nav ul { |
||||
list-style-position: inside; |
||||
padding: 0; |
||||
} |
||||
|
||||
nav ul li { |
||||
list-style: none; |
||||
display: inline-block; |
||||
padding-left: 1em; |
||||
padding-right: 1em; |
||||
} |
||||
</style> |
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
<template lang="html"> |
||||
<div class="full"> |
||||
<v-map :zoom="zoom.defaultZoom" :center="center" :bounds="bounds" :min-zoom="zoom.minZoom" :max-zoom="zoom.maxZoom"> |
||||
<v-tilelayer :url="tiles.url" :attribution="tiles.attribution"></v-tilelayer> |
||||
<template v-for="marker in flats"> |
||||
<v-marker :lat-lng="{ lat: marker.gps[0], lng: marker.gps[1] }" :icon="icons.flat"> |
||||
<v-popup :content="marker.content"></v-popup> |
||||
</v-marker> |
||||
</template> |
||||
<template v-for="(place_gps, place_name) in places"> |
||||
<v-marker :lat-lng="{ lat: place_gps[0], lng: place_gps[1] }" :icon="icons.place"> |
||||
<v-tooltip :content="place_name"></v-tooltip> |
||||
</v-marker> |
||||
</template> |
||||
</v-map> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import L from 'leaflet' |
||||
import 'leaflet/dist/leaflet.css' |
||||
import markerUrl from 'leaflet/dist/images/marker-icon.png' |
||||
import marker2XUrl from 'leaflet/dist/images/marker-icon.png' |
||||
import shadowUrl from 'leaflet/dist/images/marker-icon.png' |
||||
|
||||
require('leaflet.icon.glyph') |
||||
|
||||
import Vue2Leaflet from 'vue2-leaflet' |
||||
|
||||
export default { |
||||
data () { |
||||
return { |
||||
center: null, |
||||
zoom: { |
||||
defaultZoom: 13, |
||||
minZoom: 11, |
||||
maxZoom: 17 |
||||
}, |
||||