From 5f47b0ff65a8adfa7b93eb6910cdd24dd6e0abc2 Mon Sep 17 00:00:00 2001 From: "Phyks (Lucas Verney)" Date: Tue, 25 Apr 2017 15:58:06 +0200 Subject: [PATCH] Write some documentation --- .gitignore | 2 +- CONTRIBUTING.md | 3 +- README.md | 58 +---- doc/0.getting_started.md | 106 +++++++++ doc/1.production.md | 223 ++++++++++++++++++ flatisfy/web/app.py | 5 +- flatisfy/web/js_src/components/flatsmap.vue | 6 +- flatisfy/web/js_src/components/flatstable.vue | 2 + flatisfy/web/js_src/views/details.vue | 2 + flatisfy/web/static/index.html | 4 +- .../js/9e9c77db241e8a58da99bf28694c907d.png | Bin 535 -> 0 bytes .../js/a159160a02a1caf189f5008c0d150f36.png | Bin 16288 -> 0 bytes .../js/c6477ef24ce2054017fce1a2d8800385.png | Bin 8005 -> 0 bytes .../js/d3a5d64a8534322988a4bed1b7dbc8b0.png | Bin 1469 -> 0 bytes package.json | 10 +- webpack.config.js | 13 +- 16 files changed, 375 insertions(+), 59 deletions(-) create mode 100644 doc/0.getting_started.md create mode 100644 doc/1.production.md delete mode 100644 flatisfy/web/static/js/9e9c77db241e8a58da99bf28694c907d.png delete mode 100644 flatisfy/web/static/js/a159160a02a1caf189f5008c0d150f36.png delete mode 100644 flatisfy/web/static/js/c6477ef24ce2054017fce1a2d8800385.png delete mode 100644 flatisfy/web/static/js/d3a5d64a8534322988a4bed1b7dbc8b0.png diff --git a/.gitignore b/.gitignore index 93c23ac..8850236 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ build *.db config/ node_modules -flatisfy/web/static/js +flatisfy/web/static/assets diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df12db1..fa80f27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,8 @@ * 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`) +* Some useful `npm` scripts are provided (`build:{dev,prod}` / + `watch:{dev,prod}` / `lint`) ## Translating the webapp diff --git a/README.md b/README.md index 593814e..c00d50c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ Flatisfy Flatisfy is your new companion to ease your search of a new housing :) +**Note**: This software is under heavy development at the moment, and the +database schema could change at any time. Do not consider it as being +production ready. However, I am currently using it for my own housing search +and it is working fine :) + + It uses [Weboob](http://weboob.org/) to get all the housing posts on most of the websites offering housings posts, and then offers a bunch of pipelines to filter and deduplicate the fetched housings. @@ -29,60 +35,20 @@ This code is not restricted to handling flats only! 1. Clone the repository. 2. Install required Python modules: `pip install -r requirements.txt`. 3. Init a configuration file: `python -m flatisfy init-config > config.json`. - Edit it according to your needs (see below). + Edit it according to your needs (see doc). 4. Build the required data files: `python -m flatisfy build-data --config config.json`. 5. Use it to `fetch` (and output a filtered JSON list of flats) or `import` (into an SQLite database, for the web visualization) a list of flats matching your criteria. -6. Use `python -m flatisfy serve --config config.json` to serve the web app. +6. Install JS libraries and build the webapp: + `npm install && npm run build:dev` (use `build:prod` in production). +7. Use `python -m flatisfy serve --config config.json` to serve the web app. -## Configuration +## Documentation -List of configuration options: - -* `data_directory` is the directory in which you want data files to be stored. - `null` is the default value and means default `XDG` location (typically - `~/.local/share/flatisfy/`) -* `max_entries` is the maximum number of entries to fetch **per Weboob - backend** (that is per housing website). -* `passes` is the number of passes to run on the data. First pass is a basic - filtering and using only the informations from the housings list page. - Second pass loads any possible information about the filtered flats and does - better filtering. -* `queries` is a list of queries defined in `flatboob` that should be fetched. -* `database` is an SQLAlchemy URI to a database file. Defaults to `null` which - means that it will store the database in the default location, in - `data_directory`. -* `navitia_api_key` is an API token for [Navitia](https://www.navitia.io/) - which is required to compute travel times. - -### Constraints - -You can specify constraints, under the `constraints` key. The available -constraints are: - -* `area` (in m²), `bedrooms`, `cost` (in currency unit), `rooms`: this is a - tuple of `(min, max)` values, defining an interval in which the value should - lie. A `null` value means that any value is within this bound. -* `postal_codes` is a list of allowed postal codes. You should include any - postal code you want, and especially the postal codes close to the precise - location you want. You MUST provide some postal codes. -* `time_to` is a dictionary of places to compute travel time to them. - Typically, - ``` - "time_to": { - "foobar": { - "gps": [LAT, LNG], - "time": [min, max] - } - } - ``` - means that the housings must be between the `min` and `max` bounds (possibly - `null`) from the place identified by the GPS coordinates `LAT` and `LNG` - (latitude and longitude), and we call this place `foobar` in human-readable - form. Beware that `time` constraints are in **seconds**. +See the [dedicated folder](doc/). ## OpenData diff --git a/doc/0.getting_started.md b/doc/0.getting_started.md new file mode 100644 index 0000000..1953fce --- /dev/null +++ b/doc/0.getting_started.md @@ -0,0 +1,106 @@ +Getting started +=============== + +## TL;DR + +1. Clone the repository. +2. Install required Python modules: `pip install -r requirements.txt`. +3. Init a configuration file: `python -m flatisfy init-config > config.json`. + Edit it according to your needs (see below). +4. Build the required data files: + `python -m flatisfy build-data --config config.json`. +5. Use it to `fetch` (and output a filtered JSON list of flats) or `import` + (into an SQLite database, for the web visualization) a list of flats + matching your criteria. +6. Install JS libraries and build the webapp: + `npm install && npm run build:dev` (use `build:prod` in production). +7. Use `python -m flatisfy serve --config config.json` to serve the web app. + + +## Available commands + +The available commands are: + +* `init-config` to generate an empty configuration file, either on the `stdin` + or in the specified file. +* `fetch` to load and filter housings posts and output a JSON dump. +* `filter` to filter a previously fetched list of housings posts, provided as + a JSON dump. +* `import` to import and filter housing posts into the database. +* `serve` to serve the built-in webapp with the development server. Do not use + in production. + + +## Configuration + +List of configuration options: + +* `data_directory` is the directory in which you want data files to be stored. + `null` is the default value and means default `XDG` location (typically + `~/.local/share/flatisfy/`) +* `max_entries` is the maximum number of entries to fetch. +* `passes` is the number of passes to run on the data. First pass is a basic + filtering and using only the informations from the housings list page. + Second pass loads any possible information about the filtered flats and does + better filtering. +* `database` is an SQLAlchemy URI to a database file. Defaults to `null` which + means that it will store the database in the default location, in + `data_directory`. +* `navitia_api_key` is an API token for [Navitia](https://www.navitia.io/) + which is required to compute travel times. +* `modules_path` is the path to the Weboob modules. It can be `None` if you + want Weboob to use the locally pip-installed modules (default value). +* `port` is the port on which the development webserver should be + listening (default to `8080`). +* `host` is the host on which the development webserver should be listening + (default to `127.0.0.1`). +* `webserver` is a server to use instead of the default Bottle built-in + webserver, see [Bottle deployment + doc](http://bottlepy.org/docs/dev/deployment.html). + +_Note:_ In production, you can either use the `serve` command with a reliable +webserver instead of the default Bottle webserver (specifying a `webserver` +value) or use the `wsgi.py` script at the root of the repository to use WSGI. + + +### Constraints + +You should specify some constraints to filter the resulting housings list, +under the `constraints` key. The available constraints are: + +* `type` is the type of housing you want, either `RENT` (to rent), `SALE` (to + buy) or `SHARING` (for a shared housing). +* `housing_types` is a list of house types you are looking for. Values can be + `APART` (flat), `HOUSE`, `PARKING`, `LAND`, `OTHER` (everything else) or + `UNKNOWN` (anything which was not matched with one of the previous + categories). +* `area` (in m²), `bedrooms`, `cost` (in currency unit), `rooms`: this is a + tuple of `(min, max)` values, defining an interval in which the value should + lie. A `null` value means that any value is within this bound. +* `postal_codes` is a list of postal codes. You should include any postal code + you want, and especially the postal codes close to the precise location you + want. +* `time_to` is a dictionary of places to compute travel time to them. + Typically, + + ``` + "time_to": { + "foobar": { + "gps": [LAT, LNG], + "time": [min, max] + } + } + ``` + + means that the housings must be between the `min` and `max` bounds (possibly + `null`) from the place identified by the GPS coordinates `LAT` and `LNG` + (latitude and longitude), and we call this place `foobar` in human-readable + form. Beware that `time` constraints are in **seconds**. + + +## Building the web assets + +If you want to build the web assets, you can use `npm run build:dev` +(respectively `npm run watch:dev` to build continuously and monitor changes in +source files). You can use `npm run build:prod` (`npm run watch:prod`) to do +the same in production mode (with minification etc). diff --git a/doc/1.production.md b/doc/1.production.md new file mode 100644 index 0000000..a8df4cf --- /dev/null +++ b/doc/1.production.md @@ -0,0 +1,223 @@ +Use in production environment +============================= + +This is the guide for the manual installation of a "production" instance. + +**IMPORTANT**: At the moment, there is **no** authentication mechanism in +Flatisfy. That is, if you want your instance to be private, you should put +some access control on your own with the webserver. This is explained below in +the case of `Nginx`. + +## Get the app installed + +```bash +# Clone the repository +git clone https://Phyks@git.phyks.me/Phyks/flatisfy.git && cd flatisfy + +# Create a virtualenv +virtualenv .env && source .env/bin/activate + +# Install required Python modules +pip install -r requirements.txt + +# Clone and install weboob +git clone https://git.weboob.org/weboob/devel weboob && cd weboob && python setup.py install && cd .. + +# Install required JS libraries and build the webapp +npm install && npm run build:prod + +# Create a minimal config file +mkdir config && python -m flatisfy init-config > config/config.json + +# Create a data directory to store db and data files +mkdir data + +# Edit the config file according to your needs +$EDITOR config/config.json + +# Build the required data files +python -m flatisfy build-data --config config/config.json -v + +# Run initial import +python -m flatisfy import --config config/config.json -v +``` + +_Note_: In the config, you should set `data_directory` to the absolute path of +the `data` directory created below. This directory should be writable by the +user running Flatisfy. You should also set `modules_path` to the absolute path +to the `modules` folder under the previous `weboob` clone. Finally, the last +`import` command can be `cron`-tasked to automatically fetch available +housings posts periodically. + + +## Use an alternative Bottle backend (production) + +This is the simplest option, offers good performances but is less scalable. +You should take care of which user is running `flatisfy`, as there can be some +related security concerns. + +Just choose another [available +webserver](https://bottlepy.org/docs/dev/deployment.html) to use in Bottle +(typically `cherrypi`) and set the `webserver` config option accordingly to +use it. + +### Nginx vhost + +Here is a typical minimal `Nginx` vhost you can use (which provides +HTTP authentication support and SSL): + +``` +upstream _flatisfy { + server 127.0.0.1:PORT; +} + +server { + listen 443; + server_name SERVER_NAME; + root ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT/flatisfy/web/static; + + access_log /var/log/nginx/flatisfy-access.log; + error_log /var/log/nginx/flatisfy-error.log warn; + + ssl on; + ssl_certificate PATH_TO_SSL_CERT; + ssl_certificate_key PATH_TO_SSL_KEY; + + location / { + auth_basic "Restricted"; + auth_basic_user_file ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT/.htpasswd; + + # Try to serve directly the URI first (static content), fallback on + # passing it to the Python backend + try_files $uri @uwsgi; + } + + location @uwsgi { + proxy_pass http://_flatisfy; + } +} + + +server { + listen 80; + server_name SERVER_NAME; + + root /dev/null; + + return 301 https://$server_name$request_uri; +} +``` + +where `PORT` (default for Flatisfy is 8080), `SERVER_NAME`, +`ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT`, `PATH_TO_SSL_CERT` and `PATH_TO_SSL_KEY` +should be replaced by values according to your own setup. You should also set +the `.htpasswd` file with users and credentials. + +_Note_: This vhost is really minimalistic and you should adapt it to your +setup, enforce SSL ciphers for increased security and do such good practices +things. + + +## Use WSGI + +This is the best option in terms of performance and is recommended. It should +offer both the best security and performances. + +### Configure uWSGI + +Assuming you are running Debian stable, you should install `uwsgi`: + +``` +apt-get install uwsgi uwsgi-plugin-python +``` + +Then, you can create a `/etc/uwsgi/apps-available/flatisfy.ini` with the +following content: + +```ini +[uwsgi] +socket = /run/uwsgi/app/flatisfy/socket +chdir = ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT +master = true +plugins = python +venv = ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT/.env +file = wsgi.py +uid = www-data +gid = www-data +``` + +where `ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT` is the absolute path to the root of +the Flatisfy git clone made at the beginning of this document. + +Now, you can enable the app: + +```bash +ln -s /etc/uwsgi/apps-available/flatisfy.ini /etc/uwsgi/apps-enabled/flatisfy.ini +``` + +and restart `uWSGI`: + +```bash +systemctl restart uwsgi +``` + +_Note_: You should review the `wsgi.py` file at the root of the repository to +check it is matching you setup (and especially is loading the configuration +from the right place, which is `config/config.json` by default). + + +### Nginx vhost + +Here is a typical minimal `Nginx` vhost you can use (which provides +HTTP authentication support and SSL): + +``` +upstream _flatisfy { + server unix:/run/uwsgi/app/flatisfy/socket; +} + +server { + listen 443; + server_name SERVER_NAME; + root ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT/flatisfy/web/static; + + access_log /var/log/nginx/flatisfy-access.log; + error_log /var/log/nginx/flatisfy-error.log warn; + + ssl on; + ssl_certificate PATH_TO_SSL_CERT; + ssl_certificate_key PATH_TO_SSL_KEY; + + location / { + auth_basic "Restricted"; + auth_basic_user_file ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT/.htpasswd; + + # Try to serve directly the URI first (static content), fallback on + # passing it to the Python backend + try_files $uri @uwsgi; + } + + location @uwsgi { + include uwsgi_params; + uwsgi_pass _flatisfy; + } +} + + +server { + listen 80; + server_name SERVER_NAME; + + root /dev/null; + + return 301 https://$server_name$request_uri; +} +``` + +where `SERVER_NAME`, `ABSOLUTE_PATH_TO_FLATISFY_GIT_ROOT`, `PATH_TO_SSL_CERT` +and `PATH_TO_SSL_KEY` should be replaced by values according to your own +setup. You should also set the `.htpasswd` file with users and credentials. + +_Note_: This vhost is really minimalistic and you should adapt it to your +setup, enforce SSL ciphers for increased security and do such good practices +things. diff --git a/flatisfy/web/app.py b/flatisfy/web/app.py index 46dad4c..220d426 100644 --- a/flatisfy/web/app.py +++ b/flatisfy/web/app.py @@ -83,6 +83,9 @@ def get_app(config): app.route("/", "GET", lambda: _serve_static_file("index.html")) # Static files - app.route("/static/", "GET", _serve_static_file) + app.route( + "/assets/", "GET", + lambda filename: _serve_static_file("/assets/{}".format(filename)) + ) return app diff --git a/flatisfy/web/js_src/components/flatsmap.vue b/flatisfy/web/js_src/components/flatsmap.vue index 69b155d..d8d9465 100644 --- a/flatisfy/web/js_src/components/flatsmap.vue +++ b/flatisfy/web/js_src/components/flatsmap.vue @@ -42,9 +42,9 @@ export default { }, icons: { flat: L.icon({ - iconUrl: '/static/js/' + markerUrl, - iconRetinaUrl: '/static/js' + marker2XUrl, - shadowUrl: '/static/js' + shadowUrl + iconUrl: markerUrl, + iconRetinaUrl: marker2XUrl, + shadowUrl: shadowUrl }), place: L.icon.glyph({ prefix: 'fa', diff --git a/flatisfy/web/js_src/components/flatstable.vue b/flatisfy/web/js_src/components/flatstable.vue index f714900..0512a6d 100644 --- a/flatisfy/web/js_src/components/flatstable.vue +++ b/flatisfy/web/js_src/components/flatstable.vue @@ -52,6 +52,8 @@ + diff --git a/flatisfy/web/static/js/9e9c77db241e8a58da99bf28694c907d.png b/flatisfy/web/static/js/9e9c77db241e8a58da99bf28694c907d.png deleted file mode 100644 index 33cf95504706aa9d06ad40dbcd2fa168cbd43d13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 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 C9D%U_ diff --git a/flatisfy/web/static/js/d3a5d64a8534322988a4bed1b7dbc8b0.png b/flatisfy/web/static/js/d3a5d64a8534322988a4bed1b7dbc8b0.png deleted file mode 100644 index 1116503f6a572bec04496f164270dbbdffaefb84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1469 zcmV;u1w#6XP)$jsrRkUQsMcbX>z|j`P_4F?%Q_@f8Trt_r4(hxNiEFbHd*`e3wA~&9LoB z|JTX7kq}paKe_vc2M*ug64EAp8fjKield;^5n(28CRtDv6+dL~Fp+@_%zJ24`YVVq zBpE?@#aJOC0|_EPCT%8Af=BS!Pl8hiNCud49YlR>}1Tb-#*dPo+ z)$qjd<2BSi`Tw*-+e7qSQIZB2F9|{pVj$PjAteZrRFvox2~1WdF&r3yIy5pE{RB@l zFz=xq`k=pIC2L3tDv2DDqf#RRd?YC%r9h(yK?V}y#2Ne;Gy~%>VVE>brD8req55dj z?`V{SpsL6rIix0C3`wz4Xz)=K5@jHwsV3J7Ob2FQ7TONa5@{P|X@8nNrj3)41?_i<^tV759Srm^CO?9y)MiF(vXB8C1-?G z4cXS3j7SqpABu$-r~P)!vew(g&Y!umz$z!D6f{YWcMIh87s;8WY?KYzKA%uM*i&Y{OPyGq6Fs zND?_l42fYWNsXzKmMY_qTXxY&%XNuCjBL%1X}m$FZ3eW=ptqRhe&8VRI&f&=b<3}T z1K5LI+Cvh8kVC&qEUWpL7FA!ySwbVa=-RE?Ip857O-z|6KBeO>gNjo2nB;NbByi?% z3MX(JI8xFMkh^EZkQ$^&o$0EiOsQg9XZcj)^09RVjFdDn{Yp#)^G&{$dE&gm1P+<# zo50(^?-~5Y|GnjX=g5kJ4kbN^VM$SQ){wHO>Pp)zora`@9f+DD%g0H|4aC%l=nOl; z3IpF~a_0zmANa#?!RzmMPm~i=bVOpJl${JAOZ3}STl1s{Vdu#uM5J6@UE$=qt=bne zOO#ic!0RXl373Hn8eaQT1d$*~&9M@*tCr6TzJNuubXrJ>+i}fo$8t_#>G&xTG05@7 zm>N@zTeV8eLY5dN|5xB+;BWZAYf;QQ5_3+{jxv(6ri46KLZ(HCq{JNcNr?!t7gbh< z6?4zvZ`V?Mn~s<_O!!hufsZON?^RMxV_hL7+p);1TG9)2)Y)X*oMdfrE&4wW1rZ^n zh!Hs)@aVXzcBLN0cav}WloVXV)W9Xni`qg;ST?c1svT!ZEt$4mGA1Exza}AW9T6Nc z{!;Q%!?!W0&M|Y;nRT@*ovGq`0U|^aa-b5jo*|^;FBv(Hq%Ilw&tHBqg51N%A+JVi zrkTVtliOuUoFL!~ObubVu+t#7r_Lgi=5S`W-GTINSA^sclX#_x7zR_~TdOmHw%VOG zMGmK4Io0k^6jY~%^!?vr?AuzkMzhjgZiQ8>@H*G=ssxgX6?CU+ywRihMBIh2`n_z%eLT1;Q&-3)SwdsU@p zOzw5n)gm%XGRpSW-lik(1F^doLuy10r(wDOt*~=EVe(*Z=-Fl+8U1cCHGf}TjQ{(| z$|>1rNs5Y1hb^X72z)JK?4eJd957LN%xWK