├── .dockerignore
├── doc
├── _static
│ └── .gitkeep
├── img
│ ├── home.png
│ ├── details.png
│ ├── home2.png
│ └── followed.png
├── modules.rst
├── flatisfy.data_files.rst
├── flatisfy.web.routes.rst
├── index.rst
├── flatisfy.filters.rst
├── flatisfy.database.rst
├── flatisfy.models.rst
├── flatisfy.web.rst
├── 3.faq.md
├── 2.docker.md
└── flatisfy.rst
├── flatisfy
├── models
│ ├── __init__.py
│ ├── public_transport.py
│ └── postal_code.py
├── web
│ ├── __init__.py
│ ├── routes
│ │ └── __init__.py
│ ├── static
│ │ ├── favicon.ico
│ │ ├── img
│ │ │ ├── favicon-114.png
│ │ │ ├── favicon-120.png
│ │ │ ├── favicon-144.png
│ │ │ ├── favicon-150.png
│ │ │ ├── favicon-152.png
│ │ │ ├── favicon-16.png
│ │ │ ├── favicon-160.png
│ │ │ ├── favicon-180.png
│ │ │ ├── favicon-192.png
│ │ │ ├── favicon-310.png
│ │ │ ├── favicon-32.png
│ │ │ ├── favicon-57.png
│ │ │ ├── favicon-60.png
│ │ │ ├── favicon-64.png
│ │ │ ├── favicon-70.png
│ │ │ ├── favicon-72.png
│ │ │ ├── favicon-76.png
│ │ │ └── favicon-96.png
│ │ └── index.html
│ ├── js_src
│ │ ├── store
│ │ │ ├── index.js
│ │ │ ├── mutations-types.js
│ │ │ ├── actions.js
│ │ │ ├── mutations.js
│ │ │ └── getters.js
│ │ ├── main.js
│ │ ├── router
│ │ │ └── index.js
│ │ ├── tools
│ │ │ └── index.js
│ │ ├── views
│ │ │ ├── details.vue
│ │ │ ├── search.vue
│ │ │ └── home.vue
│ │ ├── i18n
│ │ │ ├── index.js
│ │ │ ├── en
│ │ │ │ └── index.js
│ │ │ └── fr
│ │ │ │ └── index.js
│ │ └── components
│ │ │ ├── app.vue
│ │ │ ├── notation.vue
│ │ │ ├── flatstableline.vue
│ │ │ ├── slider.vue
│ │ │ └── flatsmap.vue
│ ├── configplugin.py
│ ├── dbplugin.py
│ └── app.py
├── test_files
│ ├── vertical.jpg
│ ├── 124910113@seloger.jpg
│ ├── 127028739@seloger.jpg
│ ├── vertical-cropped.jpg
│ ├── 127028739-2@seloger.jpg
│ ├── 127028739-3@seloger.jpg
│ ├── 13783671@explorimmo.jpg
│ ├── 14428129@explorimmo.jpg
│ ├── 14428129-2@explorimmo.jpg
│ ├── 14428129-3@explorimmo.jpg
│ ├── 127028739-watermark@seloger.jpg
│ ├── 127028739@seloger.json
│ ├── 14428129@explorimmo.json
│ ├── 13783671@explorimmo.json
│ ├── 127963747@seloger.json
│ ├── 122509451@seloger.json
│ ├── 124910113@seloger.json
│ ├── 123314207@seloger.json
│ ├── 123312807@seloger.json
│ └── 128358415@seloger.json
├── __init__.py
├── database
│ ├── base.py
│ ├── types.py
│ └── __init__.py
├── exceptions.py
├── constants.py
├── filters
│ ├── images.py
│ └── cache.py
├── data.py
└── email.py
├── migrations
├── README
├── versions
│ ├── 8155b83242eb_add_is_expired.py
│ ├── 9e58c66f1ac1_add_flat_insee_column.py
│ └── d21933db9ad8_add_flat_position_column.py
├── script.py.mako
└── env.py
├── modules
├── pap
│ ├── __init__.py
│ ├── favicon.png
│ ├── constants.py
│ ├── module.py
│ ├── browser.py
│ └── test.py
├── foncia
│ ├── favicon.png
│ ├── constants.py
│ ├── __init__.py
│ ├── browser.py
│ ├── module.py
│ └── test.py
├── seloger
│ ├── __init__.py
│ ├── favicon.png
│ ├── constants.py
│ ├── module.py
│ ├── browser.py
│ └── test.py
├── leboncoin
│ ├── favicon.png
│ ├── __init__.py
│ ├── module.py
│ └── test.py
├── logicimmo
│ ├── favicon.png
│ ├── __init__.py
│ ├── module.py
│ ├── test.py
│ └── browser.py
└── explorimmo
│ ├── __init__.py
│ ├── module.py
│ ├── browser.py
│ └── test.py
├── hooks
└── pre-commit
├── import.sh
├── .babelrc
├── .vscode
├── extensions.json
└── settings.json
├── docker
├── fetch.sh
├── docker-compose.yml
├── run.sh
├── entrypoint.sh
└── Dockerfile
├── .editorconfig
├── .gitignore
├── .eslintrc
├── requirements.txt
├── start.sh
├── .gitlab-ci.yml
├── wsgi.py
├── LICENSE.md
├── alembic.ini
├── webpack.config.js
├── package.json
└── CONTRIBUTING.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | data
2 |
--------------------------------------------------------------------------------
/doc/_static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flatisfy/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flatisfy/web/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flatisfy/web/routes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/doc/img/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/doc/img/home.png
--------------------------------------------------------------------------------
/doc/img/details.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/doc/img/details.png
--------------------------------------------------------------------------------
/doc/img/home2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/doc/img/home2.png
--------------------------------------------------------------------------------
/doc/img/followed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/doc/img/followed.png
--------------------------------------------------------------------------------
/modules/pap/__init__.py:
--------------------------------------------------------------------------------
1 | from .module import PapModule
2 |
3 | __all__ = ['PapModule']
4 |
--------------------------------------------------------------------------------
/hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | pylint --rcfile=.ci/pylintrc flatisfy
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/import.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -ev
2 | python -m flatisfy import --config config.json --new-only -v "$@"
3 |
--------------------------------------------------------------------------------
/modules/pap/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/modules/pap/favicon.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0"],
3 | "plugins": ["transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/modules/foncia/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/modules/foncia/favicon.png
--------------------------------------------------------------------------------
/modules/seloger/__init__.py:
--------------------------------------------------------------------------------
1 | from .module import SeLogerModule
2 |
3 | __all__ = ['SeLogerModule']
4 |
--------------------------------------------------------------------------------
/doc/modules.rst:
--------------------------------------------------------------------------------
1 | Flatisfy
2 | ========
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | flatisfy
8 |
--------------------------------------------------------------------------------
/modules/leboncoin/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/modules/leboncoin/favicon.png
--------------------------------------------------------------------------------
/modules/logicimmo/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/modules/logicimmo/favicon.png
--------------------------------------------------------------------------------
/modules/seloger/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/modules/seloger/favicon.png
--------------------------------------------------------------------------------
/flatisfy/web/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/favicon.ico
--------------------------------------------------------------------------------
/flatisfy/test_files/vertical.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/vertical.jpg
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-114.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-120.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-144.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-150.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-152.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-16.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-160.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-160.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-180.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-192.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-310.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-32.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-57.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-60.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-64.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-70.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-72.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-76.png
--------------------------------------------------------------------------------
/flatisfy/web/static/img/favicon-96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/web/static/img/favicon-96.png
--------------------------------------------------------------------------------
/flatisfy/test_files/124910113@seloger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/124910113@seloger.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/127028739@seloger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/127028739@seloger.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/vertical-cropped.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/vertical-cropped.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/127028739-2@seloger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/127028739-2@seloger.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/127028739-3@seloger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/127028739-3@seloger.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/13783671@explorimmo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/13783671@explorimmo.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/14428129@explorimmo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/14428129@explorimmo.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/14428129-2@explorimmo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/14428129-2@explorimmo.jpg
--------------------------------------------------------------------------------
/flatisfy/test_files/14428129-3@explorimmo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/14428129-3@explorimmo.jpg
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "mtxr.sqltools",
4 | "mtxr.sqltools-driver-sqlite"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/flatisfy/test_files/127028739-watermark@seloger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Phyks/Flatisfy/HEAD/flatisfy/test_files/127028739-watermark@seloger.jpg
--------------------------------------------------------------------------------
/flatisfy/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | ``Flatisfy`` is a tool to help you find a new housing based on some criteria.
4 | """
5 | __version__ = "0.1"
6 |
--------------------------------------------------------------------------------
/docker/fetch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | echo "Fetching new housing posts..."
5 | cd /home/user/app
6 | python -m flatisfy import -v --config /flatisfy/config.json
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | insert_final_newline = true
8 |
9 | [*.py]
10 | max_line_length=120
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | *.pyc
3 | *.swp
4 | *.swo
5 | *.db
6 | config/
7 | node_modules
8 | flatisfy/web/static/assets
9 | data/
10 | doc/_build
11 | data_rework/
12 | .env
13 | .htpasswd
14 |
--------------------------------------------------------------------------------
/doc/flatisfy.data_files.rst:
--------------------------------------------------------------------------------
1 | flatisfy.data_files package
2 | ===========================
3 |
4 | Module contents
5 | ---------------
6 |
7 | .. automodule:: flatisfy.data_files
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | app:
4 | build: .
5 | # image: phyks/flatisfy
6 | environment:
7 | - LOCAL_USER_ID=1000
8 | volumes:
9 | - ./data:/flatisfy
10 | ports:
11 | - "8080:8080"
12 | working_dir: /home/user/app
13 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | extends: ["vue", /* your other extends */],
3 | plugins: ["vue"],
4 | "env": {
5 | "browser": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 8
9 | },
10 | rules: {
11 | 'indent': ["error", 4, { 'SwitchCase': 1 }],
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | alembic
2 | appdirs
3 | arrow
4 | bottle
5 | bottle-sqlalchemy
6 | canister
7 | future
8 | imagehash
9 | mapbox
10 | pillow
11 | ratelimit
12 | requests
13 | requests_mock
14 | sqlalchemy
15 | titlecase
16 | unidecode
17 | vobject
18 | whoosh
19 | git+https://gitlab.com/woob/woob/
20 | money
21 |
--------------------------------------------------------------------------------
/flatisfy/database/base.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This module contains the definition of the declarative SQLAlchemy base.
4 | """
5 | from __future__ import absolute_import, print_function, unicode_literals
6 |
7 | from sqlalchemy.ext.declarative import declarative_base
8 |
9 |
10 | BASE = declarative_base()
11 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -ev
2 |
3 | function clean_up {
4 |
5 | # Perform program exit housekeeping
6 | kill $SERVE_PID $YARN_PID
7 | exit
8 | }
9 |
10 | python -m flatisfy serve --config config.json -v &
11 | SERVE_PID=$!
12 |
13 | yarn watch:dev &
14 | YARN_PID=$!
15 |
16 | trap clean_up SIGHUP SIGINT SIGTERM
17 |
18 | wait $SERVE_PID $YARN_PID
19 |
--------------------------------------------------------------------------------
/flatisfy/exceptions.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This module contains all the exceptions definitions for the Flatisfy-specific
4 | exceptions.
5 | """
6 | from __future__ import absolute_import, print_function, unicode_literals
7 |
8 |
9 | class DataBuildError(Exception):
10 | """
11 | Error occurring on building a data file.
12 | """
13 |
14 | pass
15 |
--------------------------------------------------------------------------------
/docker/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | echo "Building data for Flatisfy..."
5 | cd /home/user/app
6 | python -m flatisfy build-data -v --config /flatisfy/config.json
7 |
8 | echo "Fetching new housing posts..."
9 | cd /home/user/app
10 | python -m flatisfy import -v --config /flatisfy/config.json
11 |
12 | echo "Starting web UI..."
13 | python -m flatisfy serve -v --config /flatisfy/config.json
14 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import actions from './actions'
5 | import getters from './getters'
6 | import { state, mutations } from './mutations'
7 | // import products from './modules/products'
8 |
9 | Vue.use(Vuex)
10 |
11 | export default new Vuex.Store({
12 | state,
13 | actions,
14 | getters,
15 | mutations
16 | })
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "woob",
4 | "flatisfy"
5 | ],
6 | "sqltools.useNodeRuntime": true,
7 | "sqltools.connections": [
8 | {
9 | "previewLimit": 50,
10 | "driver": "SQLite",
11 | "name": "flatisfy",
12 | "database": "${workspaceFolder:flatisfy}/data/flatisfy.db"
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import i18n from './i18n'
4 | import router from './router'
5 | import store from './store'
6 | import { costFilter } from './tools'
7 |
8 | import App from './components/app.vue'
9 |
10 | Vue.filter('cost', costFilter)
11 |
12 | new Vue({
13 | i18n,
14 | router,
15 | store,
16 | render: createEle => createEle(App)
17 | }).$mount('#app')
18 |
--------------------------------------------------------------------------------
/modules/seloger/constants.py:
--------------------------------------------------------------------------------
1 | from woob.capabilities.housing import POSTS_TYPES, HOUSE_TYPES
2 |
3 | TYPES = {POSTS_TYPES.RENT: 1,
4 | POSTS_TYPES.SALE: 2,
5 | POSTS_TYPES.FURNISHED_RENT: 1,
6 | POSTS_TYPES.VIAGER: 5}
7 |
8 | RET = {HOUSE_TYPES.HOUSE: '2',
9 | HOUSE_TYPES.APART: '1',
10 | HOUSE_TYPES.LAND: '4',
11 | HOUSE_TYPES.PARKING: '3',
12 | HOUSE_TYPES.OTHER: '10'}
13 |
--------------------------------------------------------------------------------
/doc/flatisfy.web.routes.rst:
--------------------------------------------------------------------------------
1 | flatisfy.web.routes package
2 | ===========================
3 |
4 | Submodules
5 | ----------
6 |
7 | flatisfy.web.routes.api module
8 | ------------------------------
9 |
10 | .. automodule:: flatisfy.web.routes.api
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 |
16 | Module contents
17 | ---------------
18 |
19 | .. automodule:: flatisfy.web.routes
20 | :members:
21 | :undoc-members:
22 | :show-inheritance:
23 |
--------------------------------------------------------------------------------
/modules/pap/constants.py:
--------------------------------------------------------------------------------
1 | from woob.capabilities.housing import POSTS_TYPES, HOUSE_TYPES
2 |
3 | TYPES = {POSTS_TYPES.RENT: 'location',
4 | POSTS_TYPES.FURNISHED_RENT: 'location',
5 | POSTS_TYPES.SALE: 'vente',
6 | POSTS_TYPES.VIAGER: 'vente'}
7 |
8 | RET = {HOUSE_TYPES.HOUSE: 'maison',
9 | HOUSE_TYPES.APART: 'appartement',
10 | HOUSE_TYPES.LAND: 'terrain',
11 | HOUSE_TYPES.PARKING: 'garage-parking',
12 | HOUSE_TYPES.OTHER: 'divers'}
13 |
--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Add local user
5 | # Either use the LOCAL_USER_ID if passed in at runtime or
6 | # fallback
7 | USER_ID=${LOCAL_USER_ID:-9001}
8 |
9 | echo "[ENTRYPOINT] Starting with UID : $USER_ID"
10 | usermod -u $USER_ID -o user
11 | export HOME=/home/user
12 |
13 | echo "[ENTRYPOINT] Setting fake values for git config..."
14 | git config --global user.email flatisfy@example.com
15 | git config --global user.name "Flatisfy Root"
16 |
17 | exec su user -c "$@"
18 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/store/mutations-types.js:
--------------------------------------------------------------------------------
1 | export const REPLACE_FLATS = 'REPLACE_FLATS'
2 | export const MERGE_FLATS = 'MERGE_FLATS'
3 | export const UPDATE_FLAT_STATUS = 'UPDATE_FLAT_STATUS'
4 | export const UPDATE_FLAT_NOTES = 'UPDATE_FLAT_NOTES'
5 | export const UPDATE_FLAT_NOTATION = 'UPDATE_FLAT_NOTATION'
6 | export const UPDATE_FLAT_VISIT_DATE = 'UPDATE_FLAT_VISIT_DATE'
7 | export const RECEIVE_TIME_TO_PLACES = 'RECEIVE_TIME_TO_PLACES'
8 | export const RECEIVE_METADATA = 'RECEIVE_METADATA'
9 | export const IS_LOADING = 'IS_LOADING'
10 |
--------------------------------------------------------------------------------
/migrations/versions/8155b83242eb_add_is_expired.py:
--------------------------------------------------------------------------------
1 | """Add is_expired
2 |
3 | Revision ID: 8155b83242eb
4 | Revises:
5 | Create Date: 2018-10-16 22:51:25.442678
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = "8155b83242eb"
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | op.add_column("flats", sa.Column("is_expired", sa.Boolean(), default=False))
21 |
22 |
23 | def downgrade():
24 | op.drop_column("flats", "is_expired")
25 |
--------------------------------------------------------------------------------
/migrations/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/migrations/versions/9e58c66f1ac1_add_flat_insee_column.py:
--------------------------------------------------------------------------------
1 | """Add flat INSEE column
2 |
3 | Revision ID: 9e58c66f1ac1
4 | Revises: d21933db9ad8
5 | Create Date: 2021-02-08 16:31:18.961186
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = "9e58c66f1ac1"
14 | down_revision = "d21933db9ad8"
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | op.add_column("postal_codes", sa.Column("insee_code", sa.String()))
21 |
22 |
23 | def downgrade():
24 | op.drop_column("postal_codes", "insee_code")
25 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. Flatisfy documentation master file, created by
2 | sphinx-quickstart on Tue Dec 5 14:21:46 2017.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Flatisfy's documentation!
7 | ====================================
8 |
9 | .. automodule:: flatisfy
10 |
11 | .. toctree::
12 |
13 | 0.getting_started.md
14 | 1.production.md
15 | 2.docker.md
16 | 3.faq.md
17 | modules.rst
18 |
19 |
20 | Indices and tables
21 | ==================
22 |
23 | * :ref:`genindex`
24 | * :ref:`modindex`
25 | * :ref:`search`
26 |
27 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | before_script:
2 | - "pip install -r requirements.txt"
3 | - "pip install pylint"
4 | - "curl -sL https://deb.nodesource.com/setup_10.x | bash -"
5 | - "apt-get install -y nodejs jq"
6 | - "npm install"
7 |
8 | lint:
9 | image: "python:3"
10 | stage: "test"
11 | script:
12 | - "hooks/pre-commit"
13 |
14 | test:
15 | image: "python:3"
16 | stage: "test"
17 | script:
18 | - python -m flatisfy init-config | jq '.constraints.default.house_types = ["APART"] | .constraints.default.type = "RENT" | .constraints.default.postal_codes = ["75014"]' > /tmp/config.json
19 | - python -m flatisfy test --config /tmp/config.json
20 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueRouter from 'vue-router'
3 |
4 | import Home from '../views/home.vue'
5 | import Status from '../views/status.vue'
6 | import Details from '../views/details.vue'
7 | import Search from '../views/search.vue'
8 |
9 | Vue.use(VueRouter)
10 |
11 | export default new VueRouter({
12 | routes: [
13 | { path: '/', component: Home, name: 'home' },
14 | { path: '/new', redirect: '/' },
15 | { path: '/status/:status', component: Status, name: 'status' },
16 | { path: '/flat/:id', component: Details, name: 'details' },
17 | { path: '/search', component: Search, name: 'search' }
18 | ]
19 | })
20 |
--------------------------------------------------------------------------------
/flatisfy/constants.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | Constants used across the app.
4 | """
5 | from __future__ import absolute_import, print_function, unicode_literals
6 |
7 | from enum import Enum
8 |
9 | # Some backends give more infos than others. Here is the precedence we want to
10 | # use. First is most important one, last is the one that will always be
11 | # considered as less trustable if two backends have similar info about a
12 | # housing.
13 | BACKENDS_BY_PRECEDENCE = [
14 | "foncia",
15 | "seloger",
16 | "pap",
17 | "leboncoin",
18 | "explorimmo",
19 | "logicimmo",
20 | ]
21 |
22 |
23 | class TimeToModes(Enum):
24 | PUBLIC_TRANSPORT = -1
25 | WALK = 1
26 | BIKE = 2
27 | CAR = 3
28 |
--------------------------------------------------------------------------------
/wsgi.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | Expose a WSGI-compatible application to serve with a webserver.
4 | """
5 | from __future__ import absolute_import, print_function, unicode_literals
6 |
7 | import logging
8 | import os
9 | import sys
10 |
11 | import flatisfy.config
12 | from flatisfy.web import app as web_app
13 |
14 |
15 | class Args:
16 | config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "config/config.json")
17 |
18 |
19 | LOGGER = logging.getLogger("flatisfy")
20 |
21 |
22 | CONFIG = flatisfy.config.load_config(Args())
23 | if CONFIG is None:
24 | LOGGER.error("Invalid configuration. Exiting. Run init-config before if this is the first time you run Flatisfy.")
25 | sys.exit(1)
26 |
27 |
28 | application = app = web_app.get_app(CONFIG)
29 |
--------------------------------------------------------------------------------
/doc/flatisfy.filters.rst:
--------------------------------------------------------------------------------
1 | flatisfy.filters package
2 | ========================
3 |
4 | Submodules
5 | ----------
6 |
7 | flatisfy.filters.cache module
8 | -----------------------------
9 |
10 | .. automodule:: flatisfy.filters.cache
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | flatisfy.filters.duplicates module
16 | ----------------------------------
17 |
18 | .. automodule:: flatisfy.filters.duplicates
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | flatisfy.filters.metadata module
24 | --------------------------------
25 |
26 | .. automodule:: flatisfy.filters.metadata
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 |
32 | Module contents
33 | ---------------
34 |
35 | .. automodule:: flatisfy.filters
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/doc/flatisfy.database.rst:
--------------------------------------------------------------------------------
1 | flatisfy.database package
2 | =========================
3 |
4 | Submodules
5 | ----------
6 |
7 | flatisfy.database.base module
8 | -----------------------------
9 |
10 | .. automodule:: flatisfy.database.base
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | flatisfy.database.types module
16 | ------------------------------
17 |
18 | .. automodule:: flatisfy.database.types
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | flatisfy.database.whooshalchemy module
24 | --------------------------------------
25 |
26 | .. automodule:: flatisfy.database.whooshalchemy
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 |
32 | Module contents
33 | ---------------
34 |
35 | .. automodule:: flatisfy.database
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/doc/flatisfy.models.rst:
--------------------------------------------------------------------------------
1 | flatisfy.models package
2 | =======================
3 |
4 | Submodules
5 | ----------
6 |
7 | flatisfy.models.flat module
8 | ---------------------------
9 |
10 | .. automodule:: flatisfy.models.flat
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | flatisfy.models.postal_code module
16 | ----------------------------------
17 |
18 | .. automodule:: flatisfy.models.postal_code
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | flatisfy.models.public_transport module
24 | ---------------------------------------
25 |
26 | .. automodule:: flatisfy.models.public_transport
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 |
32 | Module contents
33 | ---------------
34 |
35 | .. automodule:: flatisfy.models
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/doc/flatisfy.web.rst:
--------------------------------------------------------------------------------
1 | flatisfy.web package
2 | ====================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | flatisfy.web.routes
10 |
11 | Submodules
12 | ----------
13 |
14 | flatisfy.web.app module
15 | -----------------------
16 |
17 | .. automodule:: flatisfy.web.app
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
22 | flatisfy.web.configplugin module
23 | --------------------------------
24 |
25 | .. automodule:: flatisfy.web.configplugin
26 | :members:
27 | :undoc-members:
28 | :show-inheritance:
29 |
30 | flatisfy.web.dbplugin module
31 | ----------------------------
32 |
33 | .. automodule:: flatisfy.web.dbplugin
34 | :members:
35 | :undoc-members:
36 | :show-inheritance:
37 |
38 |
39 | Module contents
40 | ---------------
41 |
42 | .. automodule:: flatisfy.web
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
--------------------------------------------------------------------------------
/modules/foncia/constants.py:
--------------------------------------------------------------------------------
1 | from woob.capabilities.housing import POSTS_TYPES, HOUSE_TYPES
2 |
3 | QUERY_TYPES = {
4 | POSTS_TYPES.RENT: 'location',
5 | POSTS_TYPES.SALE: 'achat',
6 | POSTS_TYPES.FURNISHED_RENT: 'location'
7 | }
8 |
9 | QUERY_HOUSE_TYPES = {
10 | HOUSE_TYPES.APART: ['appartement', 'appartement-meuble'],
11 | HOUSE_TYPES.HOUSE: ['maison'],
12 | HOUSE_TYPES.PARKING: ['parking'],
13 | HOUSE_TYPES.LAND: ['terrain'],
14 | HOUSE_TYPES.OTHER: ['chambre', 'programme-neuf',
15 | 'local-commercial', 'immeuble']
16 | }
17 |
18 | AVAILABLE_TYPES = {
19 | POSTS_TYPES.RENT: ['appartement', 'maison', 'parking', 'chambre',
20 | 'local-commercial'],
21 | POSTS_TYPES.SALE: ['appartement', 'maison', 'parking', 'local-commercial',
22 | 'terrain', 'immeuble', 'programme-neuf'],
23 | POSTS_TYPES.FURNISHED_RENT: ['appartement-meuble']
24 | }
25 |
--------------------------------------------------------------------------------
/modules/leboncoin/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from .module import LeboncoinModule
22 |
23 |
24 | __all__ = ['LeboncoinModule']
25 |
--------------------------------------------------------------------------------
/modules/logicimmo/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from .module import LogicimmoModule
22 |
23 |
24 | __all__ = ['LogicimmoModule']
25 |
--------------------------------------------------------------------------------
/modules/explorimmo/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from .module import ExplorimmoModule
22 |
23 |
24 | __all__ = ['ExplorimmoModule']
25 |
--------------------------------------------------------------------------------
/modules/foncia/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2017 Phyks (Lucas Verney)
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from __future__ import unicode_literals
21 |
22 |
23 | from .module import FonciaModule
24 |
25 |
26 | __all__ = ['FonciaModule']
27 |
--------------------------------------------------------------------------------
/flatisfy/models/public_transport.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This modules defines an SQLAlchemy ORM model for public transport opendata.
4 | """
5 | # pylint: disable=locally-disabled,invalid-name,too-few-public-methods
6 | from __future__ import absolute_import, print_function, unicode_literals
7 |
8 | import logging
9 |
10 | from sqlalchemy import Column, Float, Integer, String
11 |
12 | from flatisfy.database.base import BASE
13 |
14 |
15 | LOGGER = logging.getLogger(__name__)
16 |
17 |
18 | class PublicTransport(BASE):
19 | """
20 | SQLAlchemy ORM model to store public transport opendata.
21 | """
22 |
23 | __tablename__ = "public_transports"
24 |
25 | id = Column(Integer, primary_key=True)
26 | # Area is an identifier to prevent loading unnecessary stops. For now it is
27 | # following ISO 3166-2.
28 | area = Column(String, index=True)
29 | name = Column(String)
30 | lat = Column(Float)
31 | lng = Column(Float)
32 |
33 | def __repr__(self):
34 | return "" % self.id
35 |
--------------------------------------------------------------------------------
/flatisfy/filters/images.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | Filtering functions to handle images.
4 |
5 | This includes functions to download images.
6 | """
7 | from __future__ import absolute_import, print_function, unicode_literals
8 |
9 | import logging
10 | import os
11 |
12 | from flatisfy.filters.cache import ImageCache
13 |
14 |
15 | LOGGER = logging.getLogger(__name__)
16 |
17 |
18 | def download_images(flats_list, config):
19 | """
20 | Download images for all flats in the list, to serve them locally.
21 |
22 | :param flats_list: A list of flats dicts.
23 | :param config: A config dict.
24 | """
25 | photo_cache = ImageCache(storage_dir=os.path.join(config["data_directory"], "images"))
26 | for flat in flats_list:
27 | for photo in flat["photos"]:
28 | # Download photo
29 | image = photo_cache.get(photo["url"])
30 | # And store the local image
31 | # Only add it if fetching was successful
32 | if image:
33 | photo["local"] = photo_cache.compute_filename(photo["url"])
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Phyks (Lucas Verney)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 | MAINTAINER Phyks
3 |
4 | # Setup layout.
5 | RUN useradd -d /home/user -m -s /bin/bash -U user
6 |
7 | # Install OS dependencies.
8 | RUN apt-get update && \
9 | apt-get install -y git libffi-dev \
10 | libxml2-dev libxslt-dev libyaml-dev libtiff-dev libjpeg-dev zlib1g-dev \
11 | libfreetype6-dev libwebp-dev build-essential gcc g++ wget;
12 |
13 | # Install latest pip and python dependencies.
14 | RUN pip install -U setuptools && \
15 | pip install html2text simplejson beautifulsoup4
16 |
17 | # Install node.js.
18 | RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
19 | && apt-get install -y nodejs
20 |
21 | RUN mkdir -p /flatisfy/data
22 | VOLUME /flatisfy
23 |
24 | COPY ./*.sh /home/user/
25 |
26 | # Install Flatisfy, set up directories and permissions.
27 | RUN cd /home/user \
28 | && git clone https://framagit.org/phyks/Flatisfy.git/ ./app \
29 | && cd ./app \
30 | && pip install -r requirements.txt \
31 | && npm install \
32 | && npm run build:dev \
33 | && mkdir -p /home/user/.local/share/flatisfy \
34 | && chown user:user -R /home/user \
35 | && chmod +x /home/user/*.sh
36 |
37 | # Run server.
38 | EXPOSE 8080
39 | ENTRYPOINT ["/home/user/entrypoint.sh"]
40 | CMD ["/home/user/run.sh"]
41 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/tools/index.js:
--------------------------------------------------------------------------------
1 | export function findFlatGPS (flat) {
2 | let gps
3 |
4 | if (flat.flatisfy_position) {
5 | gps = [flat.flatisfy_position.lat, flat.flatisfy_position.lng]
6 | } else if (flat.flatisfy_stations && flat.flatisfy_stations.length > 0) {
7 | // Try to push a marker based on stations
8 | gps = [0.0, 0.0]
9 | flat.flatisfy_stations.forEach(station => {
10 | gps = [gps[0] + station.gps[0], gps[1] + station.gps[1]]
11 | })
12 | gps = [gps[0] / flat.flatisfy_stations.length, gps[1] / flat.flatisfy_stations.length]
13 | } else {
14 | // Else, push a marker based on postal code
15 | gps = flat.flatisfy_postal_code.gps
16 | }
17 |
18 | return gps
19 | }
20 |
21 | export function capitalize (string) {
22 | return string.charAt(0).toUpperCase() + string.slice(1)
23 | }
24 |
25 | export function range (n) {
26 | return [...Array(n).keys()]
27 | }
28 |
29 | export function costFilter (value, currency) {
30 | if (!value) {
31 | return 'N/A'
32 | }
33 |
34 | if (currency === 'EUR') {
35 | currency = ' €'
36 | }
37 |
38 | var valueStr = value.toString()
39 | valueStr = ' '.repeat((3 + valueStr.length) % 3) + valueStr
40 |
41 | return valueStr.match(/.{1,3}/g).join('.') + currency
42 | }
43 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/views/details.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ $t("common.loading") }}
5 |
6 |
7 |
8 |
9 |
10 |
55 |
56 |
--------------------------------------------------------------------------------
/flatisfy/test_files/127028739@seloger.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "127028739@seloger",
3 | "url": "http://www.seloger.com/annonces/achat/appartement/rennes-35/centre/127028739.htm?p=",
4 | "title": "Appartement 3 pièces 67m² - Rennes",
5 | "area": 67,
6 | "cost": 155700,
7 | "price_per_meter": 2323.8805970149256,
8 | "currency": "€",
9 | "utilities": "",
10 | "date": "2018-01-12T02:10:00",
11 | "location": "17 PLACE MARECHAL JUIN Rennes (35000)",
12 | "station": "",
13 | "text": "Exclusivité Nexity Dans un immeuble de standing, en étage élevé avec ascenseur, Appartement Type 3 de 67 m² exposé Sud / Ouest, un séjour avec balcon et double exposition vue dégagée. Deux chambres dont une avec balcon, salle de douches, WC séparé, cave et parking en sous-sol.",
14 | "phone": null,
15 | "photos": [
16 | {
17 | "id": "0an3yarge9y446j653dewxu0jwy33pmwar47k2qym.jpg",
18 | "url": "flatisfy/test_files/127028739@seloger.jpg",
19 | "data": null
20 | }
21 | ],
22 | "rooms": 3,
23 | "bedrooms": 2,
24 | "details": {
25 | "Vue": "",
26 | "Pièces": "3",
27 | "Etage": "15",
28 | "Reference": "MT0136601",
29 | "Chambres": "2",
30 | "Cave": "",
31 | "Balcon": "5 m²",
32 | "Surface": "67 m²",
33 | "Ascenseur": "",
34 | "Etages": "30",
35 | "Parking": "1",
36 | "Salle de Séjour": ""
37 | },
38 | "flatisfy": {
39 | "postal_code": "35000"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/flatisfy/models/postal_code.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This modules defines an SQLAlchemy ORM model for a postal code opendata.
4 | """
5 | # pylint: disable=locally-disabled,invalid-name,too-few-public-methods
6 | from __future__ import absolute_import, print_function, unicode_literals
7 |
8 | import logging
9 |
10 | from sqlalchemy import Column, Float, Integer, String, UniqueConstraint
11 |
12 | from flatisfy.database.base import BASE
13 |
14 |
15 | LOGGER = logging.getLogger(__name__)
16 |
17 |
18 | class PostalCode(BASE):
19 | """
20 | SQLAlchemy ORM model to store a postal code opendata.
21 | """
22 |
23 | __tablename__ = "postal_codes"
24 |
25 | id = Column(Integer, primary_key=True)
26 | # Area is an identifier to prevent loading unnecessary stops. For now it is
27 | # following ISO 3166-2.
28 | area = Column(String, index=True)
29 | postal_code = Column(String, index=True)
30 | insee_code = Column(String, index=True)
31 | name = Column(String, index=True)
32 | lat = Column(Float)
33 | lng = Column(Float)
34 | UniqueConstraint("postal_code", "name")
35 |
36 | def __repr__(self):
37 | return "" % self.id
38 |
39 | def json_api_repr(self):
40 | """
41 | Return a dict representation of this postal code object that is JSON
42 | serializable.
43 | """
44 | return {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
45 |
--------------------------------------------------------------------------------
/flatisfy/test_files/14428129@explorimmo.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "14428129@explorimmo",
3 | "url": "http://www.explorimmo.com/annonce-14428129.html",
4 | "title": "Vente appartement 3 pièces 67 m2",
5 | "area": 67,
6 | "cost": 155700,
7 | "price_per_meter": 2323.8805970149256,
8 | "currency": "EUR",
9 | "utilities": "H.C.",
10 | "date": "2017-12-05T07:40:00",
11 | "location": "17 PLACE MARECHAL JUIN Rennes 35000",
12 | "station": null,
13 | "text": "Exclusivité Nexity Dans un immeuble de standing, en étage élevé avec\nascenseur, Appartement Type 3 de 67 m² exposé Sud / Ouest, un séjour avec\nbalcon et double exposition vue dégagée. Deux chambres dont une avec balcon,\nsalle de douches, WC séparé, cave et parking en sous-sol.\n\n",
14 | "phone": null,
15 | "photos": [
16 | {
17 | "id": "f9b2da6dfa184759aa0c349edb1cd037.jpg",
18 | "url": "flatisfy/test_files/14428129@explorimmo.jpg",
19 | "data": null
20 | }
21 | ],
22 | "rooms": 3,
23 | "bedrooms": 2,
24 | "details": {
25 | "available": true,
26 | "heatingType": "",
27 | "agency": "NEXITY LAMY, 6 avenue Jean Janvier, 35000, Rennes",
28 | "bathrooms": 0,
29 | "exposure": "Non précisé",
30 | "floor": "15",
31 | "energy": "C",
32 | "bedrooms": 2,
33 | "greenhouseGasEmission": null,
34 | "isFurnished": false,
35 | "rooms": 3,
36 | "fees": 0,
37 | "creationDate": 1512455998000,
38 | "agencyFees": 0,
39 | "availabilityDate": null,
40 | "guarantee": 0
41 | },
42 | "flatisfy": {
43 | "postal_code": "35000"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/i18n/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 | import moment from 'moment'
4 |
5 | // Import translations
6 | import en from './en'
7 | import fr from './fr'
8 |
9 | Vue.use(VueI18n)
10 |
11 | export function getBrowserLocales () {
12 | let langs = []
13 |
14 | if (navigator.languages) {
15 | // Chrome does not currently set navigator.language correctly
16 | // https://code.google.com/p/chromium/issues/detail?id=101138
17 | // but it does set the first element of navigator.languages correctly
18 | langs = navigator.languages
19 | } else if (navigator.userLanguage) {
20 | // IE only
21 | langs = [navigator.userLanguage]
22 | } else {
23 | // as of this writing the latest version of firefox + safari set this correctly
24 | langs = [navigator.language]
25 | }
26 |
27 | // Some browsers does not return uppercase for second part
28 | const locales = langs.map(function (lang) {
29 | const locale = lang.split('-')
30 | return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang
31 | })
32 |
33 | return locales
34 | }
35 |
36 | const messages = {
37 | 'en': en,
38 | 'fr': fr
39 | }
40 |
41 | const locales = getBrowserLocales()
42 |
43 | var locale = 'en' // Safe default
44 | // Get best matching locale
45 | for (var i = 0; i < locales.length; ++i) {
46 | if (messages[locales[i]]) {
47 | locale = locales[i]
48 | break // Break at first matching locale
49 | }
50 | }
51 |
52 | // Set the locale for Moment.js
53 | moment.locale(locale)
54 |
55 | export default new VueI18n({
56 | locale: locale,
57 | messages
58 | })
59 |
--------------------------------------------------------------------------------
/doc/3.faq.md:
--------------------------------------------------------------------------------
1 | FAQ
2 | ===
3 |
4 | ## What happens when duplicates are detected across different backends?
5 |
6 | There is a default precedence defined for each backend. This should be defined
7 | so that the backend with highest precedence is the backend that should contain
8 | the most precise information usually.
9 |
10 | When deduplicating, the post from the backend with the highest precedence is
11 | kept and missing info is taken from the duplicate posts (precedence is used so
12 | that in case of conflicts in a field, the data from the backend with highest
13 | precedence is used). This post contains as much data as possible, and includes
14 | references to all the other "duplicate" posts. These latter duplicate posts
15 | are then simply marked as such and never shown anymore.
16 |
17 | All origins are kept in a `urls` field in the remaining post.
18 |
19 |
20 | ## Flatisfy seems to be stuck fetching posts
21 |
22 | Fetching posts can be a long process, depending on your criterias. Run the
23 | import command with `-v` argument to get a more verbose output and check
24 | things are indeed happening. If fetching the flats is still too long, try to
25 | set `max_entries` in your config to limit the number of posts fetched.
26 |
27 |
28 | ## Docker image does not start the webserver at first start?
29 |
30 | When you launch the Docker image, it first updates Woob and fetches the
31 | housing posts matching your criterias. The webserver is only started once this
32 | is done. As fetching housing posts can take a bit of time (up to 10 minutes),
33 | the webserver will not be available right away.
34 |
35 | Once everything is ready, you should see a log message in the console running
36 | the Docker image, confirming you that webserver is up and running.
37 |
--------------------------------------------------------------------------------
/flatisfy/web/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flatisfy
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/components/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Flatisfy
4 |
12 |
13 |
14 |
15 |
16 |
54 |
55 |
77 |
--------------------------------------------------------------------------------
/flatisfy/database/types.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This modules implements custom types in SQLAlchemy.
4 | """
5 | from __future__ import absolute_import, print_function, unicode_literals
6 |
7 | import json
8 |
9 | import sqlalchemy.types as types
10 |
11 |
12 | class StringyJSON(types.TypeDecorator):
13 | """
14 | Stores and retrieves JSON as TEXT for SQLite.
15 |
16 | From
17 | https://avacariu.me/articles/2016/compiling-json-as-text-for-sqlite-with-sqlalchemy.
18 |
19 | .. note ::
20 |
21 | The associated field is immutable. That is, changes to the data
22 | (typically, changing the value of a dict field) will not trigger an
23 | update on the SQL side upon ``commit`` as the reference to the object
24 | will not have been updated. One should force the update by forcing an
25 | update of the reference (by performing a ``copy`` operation on the dict
26 | for instance).
27 | """
28 |
29 | impl = types.TEXT
30 |
31 | def process_bind_param(self, value, dialect):
32 | """
33 | Process the bound param, serialize the object to JSON before saving
34 | into database.
35 | """
36 | if value is not None:
37 | value = json.dumps(value)
38 | return value
39 |
40 | def process_result_value(self, value, dialect):
41 | """
42 | Process the value fetched from the database, deserialize the JSON
43 | string before returning the object.
44 | """
45 | if value is not None:
46 | value = json.loads(value)
47 | return value
48 |
49 |
50 | # TypeEngine.with_variant says "use StringyJSON instead when
51 | # connecting to 'sqlite'"
52 | # pylint: disable=locally-disabled,invalid-name
53 | MagicJSON = types.JSON().with_variant(StringyJSON, "sqlite")
54 |
--------------------------------------------------------------------------------
/doc/2.docker.md:
--------------------------------------------------------------------------------
1 | Installing Flatisfy using Docker
2 | ================================
3 |
4 | A basic `Dockerfile` is available for rapid testing. It is still really hacky
5 | and should not be used in production.
6 |
7 |
8 | 1\. First, build the docker image:
9 |
10 | ```
11 | cd docker
12 | docker build -t phyks/flatisfy .
13 | ```
14 |
15 | 2\. Then, create some folder to store your Flatisfy data in a permanent way (it
16 | will be mount as a Docker volume in next steps), and initialize an empty
17 | config:
18 |
19 | ```
20 | mkdir flatisfy
21 | cd flatisfy
22 | FLATISFY_VOLUME=$(pwd)
23 | docker run --rm -it -e LOCAL_USER_ID=`id -u` -v $FLATISFY_VOLUME:/flatisfy phyks/flatisfy sh -c "cd /home/user/app && python -m flatisfy init-config > /flatisfy/config.json"
24 | ```
25 |
26 |
27 | 3\. Then, edit the generated `$FLATISFY_VOLUME/config.json` file according to your needs. See
28 | [0.getting_started.md](0.getting_started.md) for more infos on the
29 | configuration file format. You will have to define your constraints (at
30 | least postal codes, house type and type of post), set `data_directory` to
31 | `/flatisfy` and set `host` to `0.0.0.0` to make the web UI accessible from
32 | outside the Docker container. The rest is up to you.
33 |
34 |
35 | 4\. Finally, run the docker image to fetch flats and serve the web UI:
36 |
37 | ```
38 | docker run -it -e LOCAL_USER_ID=`id -u` -v $FLATISFY_VOLUME:/flatisfy -p 8080:8080 phyks/flatisfy
39 | ```
40 |
41 | Your Flatisfy instance is now available at `localhost:8080`!
42 |
43 |
44 | To fetch new housing posts, you should manually call
45 |
46 | ```
47 | docker run --rm -it -e LOCAL_USER_ID=`id -u` -v $FLATISFY_VOLUME:/flatisfy phyks/flatisfy /home/user/fetch.sh
48 | ```
49 |
50 | This can be done easily in a crontask on your host system, to run it typically
51 | every night.
52 |
--------------------------------------------------------------------------------
/flatisfy/test_files/13783671@explorimmo.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "13783671@explorimmo",
3 | "url": "http://www.explorimmo.com/annonce-13783671.html",
4 | "title": "Vente appartement 3 pi\u00e8ces 65 m2",
5 | "area": 65,
6 | "cost": 145275,
7 | "price_per_meter": 2235,
8 | "currency": "EUR",
9 | "utilities": "H.C.",
10 | "date": "2017-11-10T02:04:00",
11 | "location": "225 RUE DE FOUGERES Rennes 35700",
12 | "station": null,
13 | "text": "Rennes en exclusivit\u00e9 rue de Foug\u00e8res - Grand Appartement 3 pi\u00e8ces avec Balcon\ndans une copropri\u00e9t\u00e9 avec ascenseur - Travaux \u00e0 pr\u00e9voir - 2 chambres - Cave et\ngarage\n\n",
14 | "phone": null,
15 | "photos": [{
16 | "id": "cb10f556708c4e858c1a45ec1dfda623.jpg",
17 | "url": "http://thbr.figarocms.net/images/AXuL6XMCphsRrTYttb7yR2W3CCg=/560x420/filters:fill(f6f6f6):quality(80):strip_icc()/cb10f556708c4e858c1a45ec1dfda623.jpg",
18 | "data": null
19 | }, {
20 | "id": "e2696eacce2d487e99e88c2b945cee34.jpg",
21 | "url": "http://thbr.figarocms.net/images/0Va3M6bf1eFkJJzPXC--QIc6WTo=/560x420/filters:fill(f6f6f6):quality(80):strip_icc()/e2696eacce2d487e99e88c2b945cee34.jpg",
22 | "data": null
23 | }],
24 | "rooms": 3,
25 | "bedrooms": 2,
26 | "details": {
27 | "available": true,
28 | "heatingType": "",
29 | "agency": "NEXITY LAMY, 6 avenue Jean Janvier, 35000, Rennes",
30 | "bathrooms": 0,
31 | "exposure": "Non pr\u00e9cis\u00e9",
32 | "floor": "1",
33 | "energy": "E",
34 | "bedrooms": 2,
35 | "greenhouseGasEmission": null,
36 | "isFurnished": false,
37 | "rooms": 3,
38 | "fees": 0.0,
39 | "creationDate": 1507712100000,
40 | "agencyFees": 0.0,
41 | "availabilityDate": null,
42 | "guarantee": 0.0
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = migrations
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # timezone to use when rendering the date
11 | # within the migration file as well as the filename.
12 | # string value is passed to dateutil.tz.gettz()
13 | # leave blank for localtime
14 | # timezone =
15 |
16 | # max length of characters to apply to the
17 | # "slug" field
18 | #truncate_slug_length = 40
19 |
20 | # set to 'true' to run the environment during
21 | # the 'revision' command, regardless of autogenerate
22 | # revision_environment = false
23 |
24 | # set to 'true' to allow .pyc and .pyo files without
25 | # a source .py file to be detected as revisions in the
26 | # versions/ directory
27 | # sourceless = false
28 |
29 | # version location specification; this defaults
30 | # to migrations/versions. When using multiple version
31 | # directories, initial revisions must be specified with --version-path
32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions
33 |
34 | # the output encoding used when revision files
35 | # are written from script.py.mako
36 | # output_encoding = utf-8
37 |
38 | sqlalchemy.url = sqlite:///data/flatisfy.db
39 |
40 |
41 | # Logging configuration
42 | [loggers]
43 | keys = root,sqlalchemy,alembic
44 |
45 | [handlers]
46 | keys = console
47 |
48 | [formatters]
49 | keys = generic
50 |
51 | [logger_root]
52 | level = WARN
53 | handlers = console
54 | qualname =
55 |
56 | [logger_sqlalchemy]
57 | level = WARN
58 | handlers =
59 | qualname = sqlalchemy.engine
60 |
61 | [logger_alembic]
62 | level = INFO
63 | handlers =
64 | qualname = alembic
65 |
66 | [handler_console]
67 | class = StreamHandler
68 | args = (sys.stderr,)
69 | level = NOTSET
70 | formatter = generic
71 |
72 | [formatter_generic]
73 | format = %(levelname)-5.5s [%(name)s] %(message)s
74 | datefmt = %H:%M:%S
75 |
--------------------------------------------------------------------------------
/doc/flatisfy.rst:
--------------------------------------------------------------------------------
1 | flatisfy package
2 | ================
3 |
4 | Subpackages
5 | -----------
6 |
7 | .. toctree::
8 |
9 | flatisfy.data_files
10 | flatisfy.database
11 | flatisfy.filters
12 | flatisfy.models
13 | flatisfy.web
14 |
15 | Submodules
16 | ----------
17 |
18 | flatisfy.cmds module
19 | --------------------
20 |
21 | .. automodule:: flatisfy.cmds
22 | :members:
23 | :undoc-members:
24 | :show-inheritance:
25 |
26 | flatisfy.config module
27 | ----------------------
28 |
29 | .. automodule:: flatisfy.config
30 | :members:
31 | :undoc-members:
32 | :show-inheritance:
33 |
34 | flatisfy.constants module
35 | -------------------------
36 |
37 | .. automodule:: flatisfy.constants
38 | :members:
39 | :undoc-members:
40 | :show-inheritance:
41 |
42 | flatisfy.data module
43 | --------------------
44 |
45 | .. automodule:: flatisfy.data
46 | :members:
47 | :undoc-members:
48 | :show-inheritance:
49 |
50 | flatisfy.email module
51 | ---------------------
52 |
53 | .. automodule:: flatisfy.email
54 | :members:
55 | :undoc-members:
56 | :show-inheritance:
57 |
58 | flatisfy.exceptions module
59 | --------------------------
60 |
61 | .. automodule:: flatisfy.exceptions
62 | :members:
63 | :undoc-members:
64 | :show-inheritance:
65 |
66 | flatisfy.fetch module
67 | ---------------------
68 |
69 | .. automodule:: flatisfy.fetch
70 | :members:
71 | :undoc-members:
72 | :show-inheritance:
73 |
74 | flatisfy.tests module
75 | ---------------------
76 |
77 | .. automodule:: flatisfy.tests
78 | :members:
79 | :undoc-members:
80 | :show-inheritance:
81 |
82 | flatisfy.tools module
83 | ---------------------
84 |
85 | .. automodule:: flatisfy.tools
86 | :members:
87 | :undoc-members:
88 | :show-inheritance:
89 |
90 |
91 | Module contents
92 | ---------------
93 |
94 | .. automodule:: flatisfy
95 | :members:
96 | :undoc-members:
97 | :show-inheritance:
98 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/components/notation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
61 |
62 |
69 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './flatisfy/web/js_src/main.js',
3 | output: {
4 | path: __dirname + '/flatisfy/web/static/assets/',
5 | filename: 'bundle.js',
6 | publicPath: '/assets/'
7 | },
8 | module: {
9 | loaders: [
10 | {
11 | test: /\.js$/,
12 | exclude: /(node_modules|bower_components)/,
13 | use: {
14 | loader: 'babel-loader'
15 | }
16 | },
17 | {
18 | test: /\.vue$/,
19 | loader: 'vue-loader',
20 | options: {
21 | loaders: {
22 | js: 'babel-loader'
23 | }
24 | }
25 | },
26 | {
27 | test: /\.css$/,
28 | loader: 'style-loader!css-loader'
29 | },
30 | {
31 | test: /\.(jpe?g|png|gif|svg)$/i,
32 | loaders: [
33 | 'file-loader?hash=sha512&digest=hex&name=[hash].[ext]',
34 | {
35 | loader: 'image-webpack-loader',
36 | query: {
37 | bypassOnDebug: true,
38 | 'optipng': {
39 | optimizationLevel: 7
40 | },
41 | 'gifsicle': {
42 | interlaced: false
43 | }
44 | }
45 | }
46 | ]
47 | },
48 | {
49 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
50 | loader: "url-loader?limit=10000&mimetype=application/font-woff"
51 | },
52 | {
53 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
54 | loader: "file-loader"
55 | }
56 | ]
57 | },
58 | resolve: {
59 | alias: {
60 | 'masonry': 'masonry-layout',
61 | 'isotope': 'isotope-layout'
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/migrations/env.py:
--------------------------------------------------------------------------------
1 | from __future__ import with_statement
2 | from alembic import context
3 | from sqlalchemy import engine_from_config, pool
4 | from logging.config import fileConfig
5 |
6 | # this is the Alembic Config object, which provides
7 | # access to the values within the .ini file in use.
8 | config = context.config
9 |
10 | # Interpret the config file for Python logging.
11 | # This line sets up loggers basically.
12 | fileConfig(config.config_file_name)
13 |
14 | # add your model's MetaData object here
15 | # for 'autogenerate' support
16 | # from myapp import mymodel
17 | # target_metadata = mymodel.Base.metadata
18 | target_metadata = None
19 |
20 | # other values from the config, defined by the needs of env.py,
21 | # can be acquired:
22 | # my_important_option = config.get_main_option("my_important_option")
23 | # ... etc.
24 |
25 |
26 | def run_migrations_offline():
27 | """Run migrations in 'offline' mode.
28 |
29 | This configures the context with just a URL
30 | and not an Engine, though an Engine is acceptable
31 | here as well. By skipping the Engine creation
32 | we don't even need a DBAPI to be available.
33 |
34 | Calls to context.execute() here emit the given string to the
35 | script output.
36 |
37 | """
38 | url = config.get_main_option("sqlalchemy.url")
39 | context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
40 |
41 | with context.begin_transaction():
42 | context.run_migrations()
43 |
44 |
45 | def run_migrations_online():
46 | """Run migrations in 'online' mode.
47 |
48 | In this scenario we need to create an Engine
49 | and associate a connection with the context.
50 |
51 | """
52 | connectable = engine_from_config(
53 | config.get_section(config.config_ini_section),
54 | prefix="sqlalchemy.",
55 | poolclass=pool.NullPool,
56 | )
57 |
58 | with connectable.connect() as connection:
59 | context.configure(connection=connection, target_metadata=target_metadata)
60 |
61 | with context.begin_transaction():
62 | context.run_migrations()
63 |
64 |
65 | if context.is_offline_mode():
66 | run_migrations_offline()
67 | else:
68 | run_migrations_online()
69 |
--------------------------------------------------------------------------------
/migrations/versions/d21933db9ad8_add_flat_position_column.py:
--------------------------------------------------------------------------------
1 | """Add flat position column
2 |
3 | Revision ID: d21933db9ad8
4 | Revises: 8155b83242eb
5 | Create Date: 2021-02-08 16:26:37.190842
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | import sqlalchemy.types as types
11 | import json
12 |
13 |
14 | class StringyJSON(types.TypeDecorator):
15 | """
16 | Stores and retrieves JSON as TEXT for SQLite.
17 |
18 | From
19 | https://avacariu.me/articles/2016/compiling-json-as-text-for-sqlite-with-sqlalchemy.
20 |
21 | .. note ::
22 |
23 | The associated field is immutable. That is, changes to the data
24 | (typically, changing the value of a dict field) will not trigger an
25 | update on the SQL side upon ``commit`` as the reference to the object
26 | will not have been updated. One should force the update by forcing an
27 | update of the reference (by performing a ``copy`` operation on the dict
28 | for instance).
29 | """
30 |
31 | impl = types.TEXT
32 |
33 | def process_bind_param(self, value, dialect):
34 | """
35 | Process the bound param, serialize the object to JSON before saving
36 | into database.
37 | """
38 | if value is not None:
39 | value = json.dumps(value)
40 | return value
41 |
42 | def process_result_value(self, value, dialect):
43 | """
44 | Process the value fetched from the database, deserialize the JSON
45 | string before returning the object.
46 | """
47 | if value is not None:
48 | value = json.loads(value)
49 | return value
50 |
51 |
52 | # TypeEngine.with_variant says "use StringyJSON instead when
53 | # connecting to 'sqlite'"
54 | # pylint: disable=locally-disabled,invalid-name
55 | MagicJSON = types.JSON().with_variant(StringyJSON, "sqlite")
56 |
57 | # revision identifiers, used by Alembic.
58 | revision = "d21933db9ad8"
59 | down_revision = "8155b83242eb"
60 | branch_labels = None
61 | depends_on = None
62 |
63 |
64 | def upgrade():
65 | op.add_column("flats", sa.Column("flatisfy_position", MagicJSON, default=False))
66 |
67 |
68 | def downgrade():
69 | op.drop_column("flats", "flatisfy_position")
70 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/store/actions.js:
--------------------------------------------------------------------------------
1 | import * as api from '../api'
2 | import * as types from './mutations-types'
3 |
4 | export default {
5 | getAllFlats ({ commit }) {
6 | commit(types.IS_LOADING)
7 | api.getFlats(flats => {
8 | commit(types.REPLACE_FLATS, { flats })
9 | })
10 | },
11 | getFlat ({ commit }, { flatId }) {
12 | commit(types.IS_LOADING)
13 | api.getFlat(flatId, flat => {
14 | const flats = [flat]
15 | commit(types.MERGE_FLATS, { flats })
16 | })
17 | },
18 | getAllTimeToPlaces ({ commit }) {
19 | commit(types.IS_LOADING)
20 | api.getTimeToPlaces(timeToPlaces => {
21 | commit(types.RECEIVE_TIME_TO_PLACES, { timeToPlaces })
22 | })
23 | },
24 | updateFlatStatus ({ commit }, { flatId, newStatus }) {
25 | commit(types.IS_LOADING)
26 | api.updateFlatStatus(flatId, newStatus, response => {
27 | commit(types.UPDATE_FLAT_STATUS, { flatId, newStatus })
28 | })
29 | },
30 | updateFlatNotation ({ commit }, { flatId, newNotation }) {
31 | commit(types.IS_LOADING)
32 | api.updateFlatNotation(flatId, newNotation, response => {
33 | commit(types.UPDATE_FLAT_NOTATION, { flatId, newNotation })
34 | })
35 | },
36 | updateFlatNotes ({ commit }, { flatId, newNotes }) {
37 | commit(types.IS_LOADING)
38 | api.updateFlatNotes(flatId, newNotes, response => {
39 | commit(types.UPDATE_FLAT_NOTES, { flatId, newNotes })
40 | })
41 | },
42 | updateFlatVisitDate ({ commit }, { flatId, newVisitDate }) {
43 | commit(types.IS_LOADING)
44 | api.updateFlatVisitDate(flatId, newVisitDate, response => {
45 | commit(types.UPDATE_FLAT_VISIT_DATE, { flatId, newVisitDate })
46 | })
47 | },
48 | doSearch ({ commit }, { query }) {
49 | commit(types.IS_LOADING)
50 | api.doSearch(query, flats => {
51 | commit(types.REPLACE_FLATS, { flats })
52 | })
53 | },
54 | getMetadata ({ commit }) {
55 | commit(types.IS_LOADING)
56 | api.getMetadata(metadata => {
57 | commit(types.RECEIVE_METADATA, { metadata })
58 | })
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Flatisfy",
3 | "description": "Flatisfy is your new companion to ease your search of a new housing :)",
4 | "author": "Phyks (Lucas Verney) ",
5 | "license": "MIT",
6 | "version": "0.0.1",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://Phyks@git.phyks.me/Phyks/flatisfy.git"
10 | },
11 | "homepage": "https://git.phyks.me/Phyks/flatisfy",
12 | "scripts": {
13 | "build:dev": "webpack --colors --progress",
14 | "watch:dev": "webpack --colors --progress --watch",
15 | "build:prod": "NODE_ENV=production webpack --colors --progress -p",
16 | "watch:prod": "NODE_ENV=production webpack --colors --progress --watch -p",
17 | "lint": "eslint --fix --ext .js,.vue ./flatisfy/web/js_src/**",
18 | "ziparound": "cp flatisfy/data_files/laposte.json node_modules/ziparound/laposte.json && node node_modules/ziparound"
19 | },
20 | "dependencies": {
21 | "es6-promise": "^4.1.0",
22 | "font-awesome": "^4.7.0",
23 | "font-awesome-webpack": "0.0.5-beta.2",
24 | "imagesloaded": "^4.1.1",
25 | "isomorphic-fetch": "^2.2.1",
26 | "isotope-layout": "^3.0.3",
27 | "leaflet": "^1.7.1",
28 | "leaflet.icon.glyph": "^0.2.0",
29 | "masonry": "0.0.2",
30 | "moment": "^2.18.1",
31 | "vue": "^2.2.6",
32 | "vue-flatpickr-component": "^4.0.0",
33 | "vue-i18n": "^6.1.1",
34 | "vue-images-loaded": "^1.1.2",
35 | "vue-router": "^2.4.0",
36 | "vue2-leaflet": "2.6.0",
37 | "vue2-leaflet-markercluster": "^3.1.0",
38 | "vueisotope": "^3.0.0-rc",
39 | "vuex": "^2.3.0"
40 | },
41 | "devDependencies": {
42 | "babel-core": "^6.24.1",
43 | "babel-loader": "^6.4.1",
44 | "babel-plugin-transform-runtime": "^6.23.0",
45 | "babel-preset-es2015": "^6.24.1",
46 | "babel-preset-stage-0": "^6.24.1",
47 | "css-loader": "^0.28.0",
48 | "eslint": "^3.19.0",
49 | "eslint-config-vue": "^2.0.2",
50 | "eslint-plugin-vue": "^2.0.1",
51 | "file-loader": "^0.11.1",
52 | "image-webpack-loader": "^3.3.0",
53 | "less": "^2.7.2",
54 | "style-loader": "^0.16.1",
55 | "url-loader": "^0.5.8",
56 | "vue-html-loader": "^1.2.4",
57 | "vue-loader": "^11.3.4",
58 | "vue-template-compiler": "^2.2.6",
59 | "webpack": "^2.3.3",
60 | "ziparound": "1.0.0"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/modules/foncia/browser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2017 Phyks (Lucas Verney)
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from __future__ import unicode_literals
21 |
22 |
23 | from woob.browser import PagesBrowser, URL
24 |
25 | from .constants import QUERY_TYPES
26 | from .pages import CitiesPage, HousingPage, SearchPage, SearchResultsPage
27 |
28 |
29 | class FonciaBrowser(PagesBrowser):
30 | BASEURL = 'https://fr.foncia.com'
31 |
32 | cities = URL(r'/recherche/autocomplete\?term=(?P.+)', CitiesPage)
33 | housing = URL(r'/(?P[^/]+)/.*\d+.htm', HousingPage)
34 | search_results = URL(r'/(?P[^/]+)/.*', SearchResultsPage)
35 | search = URL(r'/(?P.+)', SearchPage)
36 |
37 | def get_cities(self, pattern):
38 | """
39 | Get cities matching a given pattern.
40 | """
41 | return self.cities.open(term=pattern).iter_cities()
42 |
43 | def search_housings(self, query, cities):
44 | """
45 | Search for housings matching given query.
46 | """
47 | try:
48 | query_type = QUERY_TYPES[query.type]
49 | except KeyError:
50 | return []
51 |
52 | self.search.go(type=query_type).do_search(query, cities)
53 | return self.page.iter_housings(query_type=query.type)
54 |
55 | def get_housing(self, housing):
56 | """
57 | Get specific housing.
58 | """
59 | query_type, housing = housing.split(':')
60 | self.search.go(type=query_type).find_housing(query_type, housing)
61 | return self.page.get_housing()
62 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | import * as types from './mutations-types'
4 |
5 | export const state = {
6 | flats: [],
7 | timeToPlaces: [],
8 | metadata: [],
9 | loading: 0
10 | }
11 |
12 | export const mutations = {
13 | [types.REPLACE_FLATS] (state, { flats }) {
14 | state.flats = flats
15 | state.loading -= 1
16 | },
17 | [types.MERGE_FLATS] (state, { flats }) {
18 | flats.forEach(flat => {
19 | const flatIndex = state.flats.findIndex(storedFlat => storedFlat.id === flat.id)
20 |
21 | if (flatIndex > -1) {
22 | Vue.set(state.flats, flatIndex, flat)
23 | } else {
24 | state.flats.push(flat)
25 | }
26 | })
27 | state.loading = false
28 | state.loading -= 1
29 | },
30 | [types.UPDATE_FLAT_STATUS] (state, { flatId, newStatus }) {
31 | const index = state.flats.findIndex(flat => flat.id === flatId)
32 | if (index > -1) {
33 | Vue.set(state.flats[index], 'status', newStatus)
34 | }
35 | state.loading -= 1
36 | },
37 | [types.UPDATE_FLAT_NOTES] (state, { flatId, newNotes }) {
38 | const index = state.flats.findIndex(flat => flat.id === flatId)
39 | if (index > -1) {
40 | Vue.set(state.flats[index], 'notes', newNotes)
41 | }
42 | state.loading -= 1
43 | },
44 | [types.UPDATE_FLAT_NOTATION] (state, { flatId, newNotation }) {
45 | const index = state.flats.findIndex(flat => flat.id === flatId)
46 | if (index > -1) {
47 | Vue.set(state.flats[index], 'notation', newNotation)
48 | }
49 | state.loading -= 1
50 | },
51 | [types.UPDATE_FLAT_VISIT_DATE] (state, { flatId, newVisitDate }) {
52 | const index = state.flats.findIndex(flat => flat.id === flatId)
53 | if (index > -1) {
54 | Vue.set(state.flats[index], 'visit-date', newVisitDate)
55 | }
56 | state.loading -= 1
57 | },
58 | [types.RECEIVE_TIME_TO_PLACES] (state, { timeToPlaces }) {
59 | state.timeToPlaces = timeToPlaces
60 | state.loading -= 1
61 | },
62 | [types.RECEIVE_METADATA] (state, { metadata }) {
63 | state.metadata = metadata
64 | state.loading -= 1
65 | },
66 | [types.IS_LOADING] (state) {
67 | state.loading += 1
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/modules/leboncoin/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from woob.tools.backend import Module
22 | from woob.capabilities.housing import (CapHousing, Housing, HousingPhoto)
23 | from .browser import LeboncoinBrowser
24 | from woob import __version__ as WOOB_VERSION
25 |
26 |
27 | __all__ = ['LeboncoinModule']
28 |
29 |
30 | class LeboncoinModule(Module, CapHousing):
31 | NAME = 'leboncoin'
32 | DESCRIPTION = u'search house on leboncoin website'
33 | MAINTAINER = u'Bezleputh'
34 | EMAIL = 'carton_ben@yahoo.fr'
35 | LICENSE = 'AGPLv3+'
36 | VERSION = WOOB_VERSION
37 |
38 | BROWSER = LeboncoinBrowser
39 |
40 | def create_default_browser(self):
41 | return self.create_browser()
42 |
43 | def get_housing(self, _id):
44 | return self.browser.get_housing(_id)
45 |
46 | def fill_housing(self, housing, fields):
47 | if 'phone' in fields:
48 | housing.phone = self.browser.get_phone(housing.id)
49 | fields.remove('phone')
50 |
51 | if len(fields) > 0:
52 | self.browser.get_housing(housing.id, housing)
53 |
54 | return housing
55 |
56 | def fill_photo(self, photo, fields):
57 | if 'data' in fields and photo.url and not photo.data:
58 | photo.data = self.browser.open(photo.url).content
59 | return photo
60 |
61 | def search_city(self, pattern):
62 | return self.browser.get_cities(pattern)
63 |
64 | def search_housings(self, query):
65 | return self.browser.search_housings(query, self.name)
66 |
67 | OBJECTS = {Housing: fill_housing, HousingPhoto: fill_photo}
68 |
--------------------------------------------------------------------------------
/flatisfy/web/configplugin.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This module contains a Bottle plugin to pass the config argument to any route
4 | which needs it.
5 |
6 | This module is heavily based on code from
7 | [Bottle-SQLAlchemy](https://github.com/iurisilvio/bottle-sqlalchemy) which is
8 | licensed under MIT license.
9 | """
10 | from __future__ import absolute_import, division, print_function, unicode_literals
11 |
12 | import functools
13 | import inspect
14 |
15 | import bottle
16 |
17 |
18 | class ConfigPlugin(object):
19 | """
20 | A Bottle plugin to automatically pass the config object to the routes
21 | specifying they need it.
22 | """
23 |
24 | name = "config"
25 | api = 2
26 | KEYWORD = "config"
27 |
28 | def __init__(self, config):
29 | """
30 | :param config: The config object to pass.
31 | """
32 | self.config = config
33 |
34 | def setup(self, app): # pylint: disable=locally-disabled,no-self-use
35 | """
36 | Make sure that other installed plugins don't affect the same
37 | keyword argument and check if metadata is available.
38 | """
39 | for other in app.plugins:
40 | if not isinstance(other, ConfigPlugin):
41 | continue
42 | else:
43 | raise bottle.PluginError("Found another conflicting Config plugin.")
44 |
45 | def apply(self, callback, route):
46 | """
47 | Method called on route invocation. Should apply some transformations to
48 | the route prior to returing it.
49 |
50 | We check the presence of ``self.KEYWORD`` in the route signature and
51 | replace the route callback by a partial invocation where we replaced
52 | this argument by a valid config object.
53 | """
54 | # Check whether the route needs a valid db session or not.
55 | try:
56 | callback_args = inspect.signature(route.callback).parameters
57 | except AttributeError:
58 | # inspect.signature does not exist on older Python
59 | callback_args = inspect.getargspec(route.callback).args
60 |
61 | if self.KEYWORD not in callback_args:
62 | # If no need for a db session, call the route callback
63 | return callback
64 | kwargs = {}
65 | kwargs[self.KEYWORD] = self.config
66 | return functools.partial(callback, **kwargs)
67 |
68 |
69 | Plugin = ConfigPlugin
70 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/i18n/en/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | common: {
3 | 'flats': 'flat | flats',
4 | 'loading': 'Loading…',
5 | 'Actions': 'Actions',
6 | 'More_about': 'More about',
7 | 'Remove': 'Remove',
8 | 'Restore': 'Restore',
9 | 'Original_post': 'Original post | Original posts',
10 | 'Original_post_for': 'Original post for',
11 | 'Follow': 'Follow',
12 | 'Unfollow': 'Unfollow',
13 | 'Close': 'Close',
14 | 'sortUp': 'Sort in ascending order',
15 | 'sortDown': 'Sort in descending order',
16 | 'mins': 'min | mins',
17 | 'Unknown': 'Unknown',
18 | 'expired': 'expired'
19 | },
20 | home: {
21 | 'new_available_flats': 'New available flats',
22 | 'Last_update': 'Last update:',
23 | 'show_expired_flats': 'Show expired flats'
24 | },
25 | flatListing: {
26 | 'no_available_flats': 'No available flats.',
27 | 'no_matching_flats': 'No matching flats.'
28 | },
29 | menu: {
30 | 'available_flats': 'Available flats',
31 | 'followed_flats': 'Followed flats',
32 | 'by_status': 'Flats by status',
33 | 'search': 'Search'
34 | },
35 | flatsDetails: {
36 | 'Notation': 'Note',
37 | 'Title': 'Title',
38 | 'Area': 'Area',
39 | 'Rooms': 'Rooms',
40 | 'Cost': 'Cost',
41 | 'SquareMeterCost': 'Cost / m²',
42 | 'utilities_included': '(utilities included)',
43 | 'utilities_excluded': '(utilities excluded)',
44 | 'Description': 'Description',
45 | 'First_posted': 'First posted',
46 | 'Details': 'Details',
47 | 'Metadata': 'Metadata',
48 | 'postal_code': 'Postal code',
49 | 'nearby_stations': 'Nearby stations',
50 | 'Times_to': 'Times to',
51 | 'Location': 'Location',
52 | 'Notes': 'Notes',
53 | 'Save': 'Save',
54 | 'Contact': 'Contact',
55 | 'Visit': 'Visit',
56 | 'setDateOfVisit': 'Set date of visit',
57 | 'no_phone_found': 'No phone found',
58 | 'rooms': 'room | rooms',
59 | 'bedrooms': 'bedroom | bedrooms'
60 | },
61 | status: {
62 | 'new': 'new',
63 | 'followed': 'followed',
64 | 'ignored': 'ignored',
65 | 'user_deleted': 'user deleted',
66 | 'duplicate': 'duplicate'
67 | },
68 | slider: {
69 | 'Fullscreen_photo': 'Fullscreen photo'
70 | },
71 | search: {
72 | 'input_placeholder': 'Type anything to look for…',
73 | 'Search': 'Search!'
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/flatisfy/database/__init__.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This module contains functions related to the database.
4 | """
5 | from __future__ import absolute_import, print_function, unicode_literals
6 |
7 | import sqlite3
8 |
9 | from contextlib import contextmanager
10 |
11 | from sqlalchemy import event, create_engine
12 | from sqlalchemy.engine import Engine
13 | from sqlalchemy.orm import sessionmaker
14 | from sqlalchemy.exc import OperationalError, SQLAlchemyError
15 |
16 | import flatisfy.models.flat # noqa: F401
17 | from flatisfy.database.base import BASE
18 | from flatisfy.database.whooshalchemy import IndexService
19 |
20 |
21 | @event.listens_for(Engine, "connect")
22 | def set_sqlite_pragma(dbapi_connection, _):
23 | """
24 | Auto enable foreign keys for SQLite.
25 | """
26 | # Play well with other DB backends
27 | if isinstance(dbapi_connection, sqlite3.Connection):
28 | cursor = dbapi_connection.cursor()
29 | cursor.execute("PRAGMA foreign_keys=ON")
30 | cursor.close()
31 |
32 |
33 | def init_db(database_uri=None, search_db_uri=None):
34 | """
35 | Initialize the database, ensuring tables exist etc.
36 |
37 | :param database_uri: An URI describing an engine to use. Defaults to
38 | in-memory SQLite database.
39 | :param search_db_uri: Path to the Whoosh index file to use.
40 | :return: A tuple of an SQLAlchemy session maker and the created engine.
41 | """
42 | if database_uri is None:
43 | database_uri = "sqlite:///:memory:"
44 |
45 | engine = create_engine(database_uri)
46 | BASE.metadata.create_all(engine, checkfirst=True)
47 | Session = sessionmaker(bind=engine) # pylint: disable=locally-disabled,invalid-name
48 |
49 | if search_db_uri:
50 | index_service = IndexService(whoosh_base=search_db_uri)
51 | index_service.register_class(flatisfy.models.flat.Flat)
52 |
53 | @contextmanager
54 | def get_session():
55 | # pylint: disable=locally-disabled,line-too-long
56 | """
57 | Provide a transactional scope around a series of operations.
58 |
59 | From [1].
60 | [1]: http://docs.sqlalchemy.org/en/latest/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it.
61 | """
62 | # pylint: enable=line-too-long,locally-disabled
63 | session = Session()
64 | try:
65 | yield session
66 | session.commit()
67 | except SQLAlchemyError:
68 | session.rollback()
69 | raise
70 | finally:
71 | session.close()
72 |
73 | return get_session
74 |
--------------------------------------------------------------------------------
/modules/foncia/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2017 Phyks (Lucas Verney)
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from __future__ import unicode_literals
21 |
22 |
23 | from woob.tools.backend import Module
24 | from woob.capabilities.housing import CapHousing, Housing, ADVERT_TYPES, HousingPhoto
25 | from woob import __version__ as WOOB_VERSION
26 |
27 | from .browser import FonciaBrowser
28 |
29 |
30 | __all__ = ['FonciaModule']
31 |
32 |
33 | class FonciaModule(Module, CapHousing):
34 | NAME = 'foncia'
35 | DESCRIPTION = u'Foncia housing website.'
36 | MAINTAINER = u'Phyks (Lucas Verney)'
37 | EMAIL = 'phyks@phyks.me'
38 | LICENSE = 'AGPLv3+'
39 | VERSION = WOOB_VERSION
40 |
41 | BROWSER = FonciaBrowser
42 |
43 | def get_housing(self, housing):
44 | return self.browser.get_housing(housing)
45 |
46 | def search_city(self, pattern):
47 | return self.browser.get_cities(pattern)
48 |
49 | def search_housings(self, query):
50 | if (
51 | len(query.advert_types) == 1 and
52 | query.advert_types[0] == ADVERT_TYPES.PERSONAL
53 | ):
54 | # Foncia is pro only
55 | return list()
56 |
57 | cities = ','.join(
58 | ['%s' % c.name for c in query.cities if c.backend == self.name]
59 | )
60 | if len(cities) == 0:
61 | return []
62 |
63 | return self.browser.search_housings(query, cities)
64 |
65 | def fill_housing(self, housing, fields):
66 | if len(fields) > 0:
67 | self.browser.get_housing(housing)
68 | return housing
69 |
70 | def fill_photo(self, photo, fields):
71 | if 'data' in fields and photo.url and not photo.data:
72 | photo.data = self.browser.open(photo.url).content
73 | return photo
74 |
75 | OBJECTS = {Housing: fill_housing, HousingPhoto: fill_photo}
76 |
--------------------------------------------------------------------------------
/flatisfy/test_files/127963747@seloger.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "127963747@seloger",
3 | "url": "http://www.seloger.com/annonces/achat/appartement/rennes-35/127963747.htm?p=",
4 | "title": "Appartement 3 pi\u00e8ces 78m\u00b2 - Rennes",
5 | "area": 78,
6 | "cost": 211000,
7 | "price_per_meter": 2705.128205128205128205128205,
8 | "currency": "\u20ac",
9 | "utilities": "",
10 | "date": "2018-01-17T17:54:00",
11 | "location": " Rennes (35000)",
12 | "station": "",
13 | "text": "ARSENAL/REDON - CIT\u00c9 JUDICIAIRE. D'une surface de 78 m\u00b2, cet appartement de type T3 est compos\u00e9 au rez-de-chauss\u00e9e comme suit: cuisine am\u00e9nag\u00e9e, deux chambres, salon/salle \u00e0 manger, salle de bain, toilettes.. La belle et lumineuse pi\u00e8ce de vie de 33 m\u00b2 vous permettra d'envisager une disposition agr\u00e9able de votre int\u00e9rieur.. Id\u00e9alement situ\u00e9 dans un secteur recherch\u00e9. Tr\u00e8s bon \u00e9tat.. Un garage situ\u00e9 en sous-sol compl\u00e8te cet appartement.. Contacter Agence ORPI au 02.23.44.37. 47.. 211000 euros Honoraires \u00e0 la charge du vendeur.",
14 | "phone": null,
15 | "photos": [{
16 | "id": "1d9ks91ml67r2zwwcytkg3l4jh4yc8ii3y4fa64u8.jpg",
17 | "url": "https://v.seloger.com/s/width/800/visuels/1/d/9/k/1d9ks91ml67r2zwwcytkg3l4jh4yc8ii3y4fa64u8.jpg",
18 | "data": null
19 | }, {
20 | "id": "0a95gv0bukbrk77mhe0h4n14j9bx2zrkfikgh7h8g.jpg",
21 | "url": "https://v.seloger.com/s/width/800/visuels/0/a/9/5/0a95gv0bukbrk77mhe0h4n14j9bx2zrkfikgh7h8g.jpg",
22 | "data": null
23 | }, {
24 | "id": "1hd329lc8srsdh71o3iyo2tuv8jw9jutnctvqnv9c.jpg",
25 | "url": "https://v.seloger.com/s/width/800/visuels/1/h/d/3/1hd329lc8srsdh71o3iyo2tuv8jw9jutnctvqnv9c.jpg",
26 | "data": null
27 | }, {
28 | "id": "1lf8fyr5marcjalerkc914opcc29osb23z9c9648w.jpg",
29 | "url": "https://v.seloger.com/s/width/800/visuels/1/l/f/8/1lf8fyr5marcjalerkc914opcc29osb23z9c9648w.jpg",
30 | "data": null
31 | }, {
32 | "id": "1yrk6jbek3h7q3f9a3g1vy0kqc2uh7z4yckznrx8g.jpg",
33 | "url": "https://v.seloger.com/s/width/800/visuels/1/y/r/k/1yrk6jbek3h7q3f9a3g1vy0kqc2uh7z4yckznrx8g.jpg",
34 | "data": null
35 | }],
36 | "rooms": 3,
37 | "bedrooms": 2,
38 | "details": {
39 | "Box": "1",
40 | "Pi\u00e8ces": "3",
41 | "Etage": "RDC",
42 | "Reference": "114020E0PULC",
43 | "Chambres": "2",
44 | "Chauffage": "individuel",
45 | "Toilette": "1",
46 | "Salle de bain": "1",
47 | "Ascenseur": "",
48 | "Toilettes S\u00e9par\u00e9es": "",
49 | "Surface": "78 m\u00b2",
50 | "Salle \u00c0 Manger": "",
51 | "Salle de s\u00e9jour": "33 m\u00b2"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/flatisfy/test_files/122509451@seloger.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "122509451@seloger",
3 | "url": "http://www.seloger.com/annonces/achat/appartement/rennes-35/cleunay-arsenal-redon/122509451.htm?p=",
4 | "title": "Appartement 3 pi\u00e8ces 78m\u00b2 - Rennes",
5 | "area": 78,
6 | "cost": 211000,
7 | "price_per_meter": 2705.128205128205128205128205,
8 | "currency": "\u20ac",
9 | "utilities": "",
10 | "date": "2018-01-19T22:39:00",
11 | "location": " Rennes (35000)",
12 | "station": "Arsenal - Redon",
13 | "text": "Appartement quartier Arsenal Redon, \u00e0 vendre, type 3 de 78 m\u00b2. Il se compose d'une entr\u00e9e, d'un salon-s\u00e9jour lumineux de 33 m\u00b2 orient\u00e9 sud donnant sur une terrasse, de deux chambres, d'une cuisine ind\u00e9pendante, d'une salle de bains et d'un toilette. Vous disposerez d'un garage ferm\u00e9. Situ\u00e9 entre le centre ville et la future station m\u00e9tro Mabilais (ligne B), proximit\u00e9 imm\u00e9diate des commerces, \u00e9coles.. Bien soumis au statut de la copropri\u00e9t\u00e9. Charges annuelles courantes: 962e Agence immobili\u00e8re ERA Rennes Aristide Briand Agent Commercial: Guillaume DE KERANFLECH RSAC: 818942955.",
14 | "phone": null,
15 | "photos": [{
16 | "id": "0oj57y4pvtz7537ibvjq1agi9hrpctm96o30wknpc.jpg",
17 | "url": "https://v.seloger.com/s/width/800/visuels/0/o/j/5/0oj57y4pvtz7537ibvjq1agi9hrpctm96o30wknpc.jpg",
18 | "data": null
19 | }, {
20 | "id": "0s0kr6fw0hbqkwm5m2oxhi8yysk6mfxb9ctcrx2bk.jpg",
21 | "url": "https://v.seloger.com/s/width/800/visuels/0/s/0/k/0s0kr6fw0hbqkwm5m2oxhi8yysk6mfxb9ctcrx2bk.jpg",
22 | "data": null
23 | }, {
24 | "id": "0z8q9eq4rprqfymp0mpcezrq6kxp8uxknf5pgrx8g.jpg",
25 | "url": "https://v.seloger.com/s/width/800/visuels/0/z/8/q/0z8q9eq4rprqfymp0mpcezrq6kxp8uxknf5pgrx8g.jpg",
26 | "data": null
27 | }, {
28 | "id": "01ti2ovzcuyx4e14qfqqgatynges1grnalb4eau4g.jpg",
29 | "url": "https://v.seloger.com/s/width/800/visuels/0/1/t/i/01ti2ovzcuyx4e14qfqqgatynges1grnalb4eau4g.jpg",
30 | "data": null
31 | }, {
32 | "id": "250ckvp15x8eeetuynem2kj7x8z12y66kay9okf0g.jpg",
33 | "url": "https://v.seloger.com/s/width/800/visuels/2/5/0/c/250ckvp15x8eeetuynem2kj7x8z12y66kay9okf0g.jpg",
34 | "data": null
35 | }],
36 | "rooms": 3,
37 | "bedrooms": 2,
38 | "details": {
39 | "Box": "1",
40 | "Cuisine": "s\u00e9par\u00e9e",
41 | "Pi\u00e8ces": "3",
42 | "Etage": "RDC",
43 | "Reference": "872GK-01",
44 | "Chambres": "2",
45 | "Chauffage": "individuel",
46 | "Entr\u00e9e": "",
47 | "Surface": "78 m\u00b2",
48 | "Terrasse": "1",
49 | "Etages": "5",
50 | "Salle de S\u00e9jour": ""
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/i18n/fr/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | common: {
3 | 'flats': 'appartement | appartements',
4 | 'loading': 'Chargement…',
5 | 'Actions': 'Actions',
6 | 'More_about': 'Plus sur',
7 | 'Remove': 'Enlever',
8 | 'Restore': 'Remettre',
9 | 'Original_post': 'Annonce originale | Annonces originales',
10 | 'Original_post_for': 'Annonce originale pour',
11 | 'Follow': 'Suivre',
12 | 'Unfollow': 'Arrêter de suivre',
13 | 'Close': 'Fermer',
14 | 'sortUp': 'Trier par ordre croissant',
15 | 'sortDown': 'Trier par ordre décroissant',
16 | 'mins': 'min | mins',
17 | 'Unknown': 'Inconnu',
18 | 'expired': 'expiré'
19 | },
20 | home: {
21 | 'new_available_flats': 'Nouveaux appartements disponibles',
22 | 'Last_update': 'Dernière mise à jour :',
23 | 'show_expired_flats': 'Montrer les annonces expirées'
24 | },
25 | flatListing: {
26 | 'no_available_flats': 'Pas d\'appartement disponible.',
27 | 'no_matching_flats': 'Pas d\'appartement correspondant.'
28 | },
29 | menu: {
30 | 'available_flats': 'Appartements disponibles',
31 | 'followed_flats': 'Appartements suivis',
32 | 'by_status': 'Appartements par statut',
33 | 'search': 'Rechercher'
34 | },
35 | flatsDetails: {
36 | 'Notation': 'Note',
37 | 'Title': 'Titre',
38 | 'Area': 'Surface',
39 | 'Rooms': 'Pièces',
40 | 'Cost': 'Coût',
41 | 'SquareMeterCost': 'Coût / m²',
42 | 'utilities_included': '(charges comprises)',
43 | 'utilities_excluded': '(charges non comprises)',
44 | 'Description': 'Description',
45 | 'First_posted': 'Posté pour la première fois',
46 | 'Details': 'Détails',
47 | 'Metadata': 'Metadonnées',
48 | 'postal_code': 'Code postal',
49 | 'nearby_stations': 'Stations proches',
50 | 'Times_to': 'Temps jusqu\'à',
51 | 'Location': 'Localisation',
52 | 'Notes': 'Notes',
53 | 'Save': 'Sauvegarder',
54 | 'Contact': 'Contact',
55 | 'Visit': 'Visite',
56 | 'setDateOfVisit': 'Entrer une date de visite',
57 | 'no_phone_found': 'Pas de numéro de téléphone trouvé',
58 | 'rooms': 'pièce | pièces',
59 | 'bedrooms': 'chambre | chambres'
60 | },
61 | status: {
62 | 'new': 'nouveau',
63 | 'followed': 'suivi',
64 | 'ignored': 'ignoré',
65 | 'user_deleted': 'effacé',
66 | 'duplicate': 'en double'
67 | },
68 | slider: {
69 | 'Fullscreen_photo': 'Photo en plein écran'
70 | },
71 | search: {
72 | 'input_placeholder': 'Tapez n\'importe quoi à rechercher…',
73 | 'Search': 'Chercher !'
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/flatisfy/web/dbplugin.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | This module contains a Bottle plugin to pass the database argument to any route
4 | which needs it.
5 |
6 | This module is heavily based on code from
7 | [Bottle-SQLAlchemy](https://github.com/iurisilvio/bottle-sqlalchemy) which is
8 | licensed under MIT license.
9 | """
10 | from __future__ import absolute_import, division, print_function, unicode_literals
11 |
12 | import inspect
13 |
14 | import bottle
15 |
16 |
17 | class DatabasePlugin(object):
18 | """
19 | A Bottle plugin to automatically pass an SQLAlchemy database session object
20 | to the routes specifying they need it.
21 | """
22 |
23 | name = "database"
24 | api = 2
25 | KEYWORD = "db"
26 |
27 | def __init__(self, get_session):
28 | """
29 | :param create_session: SQLAlchemy session maker created with the
30 | 'sessionmaker' function. Will create its own if undefined.
31 | """
32 | self.get_session = get_session
33 |
34 | def setup(self, app): # pylint: disable=locally-disabled,no-self-use
35 | """
36 | Make sure that other installed plugins don't affect the same
37 | keyword argument and check if metadata is available.
38 | """
39 | for other in app.plugins:
40 | if not isinstance(other, DatabasePlugin):
41 | continue
42 | else:
43 | raise bottle.PluginError("Found another conflicting Database plugin.")
44 |
45 | def apply(self, callback, route):
46 | """
47 | Method called on route invocation. Should apply some transformations to
48 | the route prior to returing it.
49 |
50 | We check the presence of ``self.KEYWORD`` in the route signature and
51 | replace the route callback by a partial invocation where we replaced
52 | this argument by a valid SQLAlchemy session.
53 | """
54 | # Check whether the route needs a valid db session or not.
55 | try:
56 | callback_args = inspect.signature(route.callback).parameters
57 | except AttributeError:
58 | # inspect.signature does not exist on older Python
59 | callback_args = inspect.getargspec(route.callback).args
60 |
61 | if self.KEYWORD not in callback_args:
62 | # If no need for a db session, call the route callback
63 | return callback
64 |
65 | def wrapper(*args, **kwargs):
66 | """
67 | Wrap the callback in a call to get_session.
68 | """
69 | with self.get_session() as session:
70 | # Get a db session and pass it to the callback
71 | kwargs[self.KEYWORD] = session
72 | return callback(*args, **kwargs)
73 |
74 | return wrapper
75 |
76 |
77 | Plugin = DatabasePlugin
78 |
--------------------------------------------------------------------------------
/modules/pap/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2012 Romain Bignon
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from woob.capabilities.housing import (CapHousing, Housing, HousingPhoto,
22 | ADVERT_TYPES)
23 | from woob.tools.backend import Module
24 | from woob import __version__ as WOOB_VERSION
25 |
26 | from .browser import PapBrowser
27 |
28 |
29 | __all__ = ['PapModule']
30 |
31 |
32 | class PapModule(Module, CapHousing):
33 | NAME = 'pap'
34 | MAINTAINER = u'Romain Bignon'
35 | EMAIL = 'romain@weboob.org'
36 | VERSION = WOOB_VERSION
37 | DESCRIPTION = 'French housing website'
38 | LICENSE = 'AGPLv3+'
39 | BROWSER = PapBrowser
40 |
41 | def search_housings(self, query):
42 | if(len(query.advert_types) == 1 and
43 | query.advert_types[0] == ADVERT_TYPES.PROFESSIONAL):
44 | # Pap is personal only
45 | return list()
46 |
47 | cities = ['%s' % c.id for c in query.cities if c.backend == self.name]
48 | if len(cities) == 0:
49 | return list()
50 |
51 | return self.browser.search_housings(query.type, cities, query.nb_rooms,
52 | query.area_min, query.area_max,
53 | query.cost_min, query.cost_max,
54 | query.house_types)
55 |
56 | def get_housing(self, housing):
57 | if isinstance(housing, Housing):
58 | id = housing.id
59 | else:
60 | id = housing
61 | housing = None
62 |
63 | return self.browser.get_housing(id, housing)
64 |
65 | def search_city(self, pattern):
66 | return self.browser.search_geo(pattern)
67 |
68 | def fill_photo(self, photo, fields):
69 | if 'data' in fields and photo.url and not photo.data:
70 | photo.data = self.browser.open(photo.url).content
71 | return photo
72 |
73 | def fill_housing(self, housing, fields):
74 | return self.browser.get_housing(housing.id, housing)
75 |
76 | OBJECTS = {HousingPhoto: fill_photo, Housing: fill_housing}
77 |
--------------------------------------------------------------------------------
/modules/explorimmo/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from woob.tools.backend import Module
22 | from woob.capabilities.housing import CapHousing, Housing, HousingPhoto
23 | from woob import __version__ as WOOB_VERSION
24 |
25 | from .browser import ExplorimmoBrowser
26 |
27 |
28 | __all__ = ['ExplorimmoModule']
29 |
30 |
31 | class ExplorimmoModule(Module, CapHousing):
32 | NAME = 'explorimmo'
33 | DESCRIPTION = u'explorimmo website'
34 | MAINTAINER = u'Bezleputh'
35 | EMAIL = 'carton_ben@yahoo.fr'
36 | LICENSE = 'AGPLv3+'
37 | VERSION = WOOB_VERSION
38 |
39 | BROWSER = ExplorimmoBrowser
40 |
41 | def get_housing(self, housing):
42 | if isinstance(housing, Housing):
43 | id = housing.id
44 | else:
45 | id = housing
46 | housing = None
47 | housing = self.browser.get_housing(id, housing)
48 | return housing
49 |
50 | def search_city(self, pattern):
51 | return self.browser.get_cities(pattern)
52 |
53 | def search_housings(self, query):
54 | cities = ['%s' % c.id for c in query.cities if c.backend == self.name]
55 | if len(cities) == 0:
56 | return list()
57 |
58 | return self.browser.search_housings(query.type, cities, query.nb_rooms,
59 | query.area_min, query.area_max,
60 | query.cost_min, query.cost_max,
61 | query.house_types,
62 | query.advert_types)
63 |
64 | def fill_housing(self, housing, fields):
65 | if 'phone' in fields:
66 | housing.phone = self.browser.get_phone(housing.id)
67 | fields.remove('phone')
68 |
69 | if len(fields) > 0:
70 | self.browser.get_housing(housing.id, housing)
71 |
72 | return housing
73 |
74 | def fill_photo(self, photo, fields):
75 | if 'data' in fields and photo.url and not photo.data:
76 | photo.data = self.browser.open(photo.url).content
77 | return photo
78 |
79 | OBJECTS = {Housing: fill_housing,
80 | HousingPhoto: fill_photo,
81 | }
82 |
--------------------------------------------------------------------------------
/flatisfy/test_files/124910113@seloger.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "124910113@seloger",
3 | "url": "http://www.seloger.com/annonces/achat/appartement/rennes-35/maurepas-patton/124910113.htm?p=",
4 | "title": "Appartement 3 pi\u00e8ces 65m\u00b2 - Rennes",
5 | "area": 65,
6 | "cost": 145275,
7 | "price_per_meter": 2235,
8 | "currency": "\u20ac",
9 | "utilities": "",
10 | "date": "2018-01-20T02:09:00",
11 | "location": "225 RUE DE FOUGERES Rennes (35700)",
12 | "station": "",
13 | "text": "Rennes en exclusivit\u00e9 rue de Foug\u00e8res - Grand Appartement 3 pi\u00e8ces avec Balcon dans une copropri\u00e9t\u00e9 avec ascenseur - Travaux \u00e0 pr\u00e9voir - 2 chambres - Cave et garage.",
14 | "phone": null,
15 | "photos": [{
16 | "id": "17b055i7hici1wxr951unlycfs5rhai73sbnnv2ki.jpg",
17 | "url": "https://v.seloger.com/s/cdn/x/visuels/1/7/b/0/17b055i7hici1wxr951unlycfs5rhai73sbnnv2ki.jpg",
18 | "data": null
19 | }, {
20 | "id": "1s5t0lal78twswu22mahad9vtc75y3s5utuit2yte.jpg",
21 | "url": "https://v.seloger.com/s/cdn/x/visuels/1/s/5/t/1s5t0lal78twswu22mahad9vtc75y3s5utuit2yte.jpg",
22 | "data": null
23 | }, {
24 | "id": "282rrcholht5full009yb8a5k1xe2jx0yiwtqyite.jpg",
25 | "url": "https://v.seloger.com/s/cdn/x/visuels/2/8/2/r/282rrcholht5full009yb8a5k1xe2jx0yiwtqyite.jpg",
26 | "data": null
27 | }, {
28 | "id": "0wskjpe0511ak2ynzxual2qa0fp3bmz3ccaoqc5oi.jpg",
29 | "url": "https://v.seloger.com/s/cdn/x/visuels/0/w/s/k/0wskjpe0511ak2ynzxual2qa0fp3bmz3ccaoqc5oi.jpg",
30 | "data": null
31 | }, {
32 | "id": "0kfne4iignt712pcunkcu2u9e497vt6oi11l30hxe.jpg",
33 | "url": "https://v.seloger.com/s/cdn/x/visuels/0/k/f/n/0kfne4iignt712pcunkcu2u9e497vt6oi11l30hxe.jpg",
34 | "data": null
35 | }, {
36 | "id": "1jvyyiua1l843w1ohymxcbs9gj9zxvtfiajjfvwle.jpg",
37 | "url": "https://v.seloger.com/s/cdn/x/visuels/1/j/v/y/1jvyyiua1l843w1ohymxcbs9gj9zxvtfiajjfvwle.jpg",
38 | "data": null
39 | }, {
40 | "id": "1ihj8ufsfdxgfecq03c154hcsj5jo5ysts29wjnia.jpg",
41 | "url": "https://v.seloger.com/s/cdn/x/visuels/1/i/h/j/1ihj8ufsfdxgfecq03c154hcsj5jo5ysts29wjnia.jpg",
42 | "data": null
43 | }, {
44 | "id": "1g9yb1xe0bc8se0w8jys8ouiscpwer6y6lccd1ltu.jpg",
45 | "url": "https://v.seloger.com/s/cdn/x/visuels/1/g/9/y/1g9yb1xe0bc8se0w8jys8ouiscpwer6y6lccd1ltu.jpg",
46 | "data": null
47 | }],
48 | "rooms": 3,
49 | "bedrooms": 2,
50 | "details": {
51 | "Box": "1",
52 | "Pi\u00e8ces": "3",
53 | "Etage": "1",
54 | "Reference": "MT0135140",
55 | "Chambres": "2",
56 | "Salle d'eau": "1",
57 | "Cave": "",
58 | "Ascenseur": "",
59 | "Surface": "65 m\u00b2",
60 | "Balcon": "1",
61 | "Travaux \u00c0 Pr\u00e9voir": "",
62 | "Ann\u00e9e de construction": "1968",
63 | "Toilettes S\u00e9par\u00e9es": "",
64 | "Etages": "6",
65 | "Toilette": "1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/modules/seloger/module.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2012 Romain Bignon
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from woob.capabilities.housing import CapHousing, Housing, HousingPhoto
22 | from woob.tools.backend import Module
23 | from woob import __version__ as WOOB_VERSION
24 |
25 | from .browser import SeLogerBrowser
26 |
27 |
28 | __all__ = ['SeLogerModule']
29 |
30 |
31 | class SeLogerModule(Module, CapHousing):
32 | NAME = 'seloger'
33 | MAINTAINER = u'Romain Bignon'
34 | EMAIL = 'romain@weboob.org'
35 | VERSION = WOOB_VERSION
36 | DESCRIPTION = 'French housing website'
37 | LICENSE = 'AGPLv3+'
38 | ICON = 'http://static.poliris.com/z/portail/svx/portals/sv6_gen/favicon.png'
39 | BROWSER = SeLogerBrowser
40 |
41 | def search_housings(self, query):
42 | cities = [c.id for c in query.cities if c.backend == self.name]
43 | if len(cities) == 0:
44 | return list([])
45 |
46 | return self.browser.search_housings(query.type, cities, query.nb_rooms,
47 | query.area_min, query.area_max,
48 | query.cost_min, query.cost_max,
49 | query.house_types,
50 | query.advert_types)
51 |
52 | def get_housing(self, housing):
53 | if isinstance(housing, Housing):
54 | id = housing.id
55 | else:
56 | id = housing
57 | housing = None
58 |
59 | return self.browser.get_housing(id, housing)
60 |
61 | def search_city(self, pattern):
62 | return self.browser.search_geo(pattern)
63 |
64 | def fill_photo(self, photo, fields):
65 | if 'data' in fields and photo.url and not photo.data:
66 | photo.data = self.browser.open(photo.url).content
67 | return photo
68 |
69 | def fill_housing(self, housing, fields):
70 |
71 | if 'DPE' in fields or 'GES' in fields:
72 | housing = self.browser.get_housing_detail(housing)
73 | fields.remove('DPE')
74 | fields.remove('GES')
75 |
76 | if len(fields) > 0:
77 | housing = self.browser.get_housing(housing.id, housing)
78 |
79 | return housing
80 |
81 | OBJECTS = {HousingPhoto: fill_photo, Housing: fill_housing}
82 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/views/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
55 |
56 |
57 |
94 |
95 |
101 |
--------------------------------------------------------------------------------
/modules/seloger/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2012 Romain Bignon
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from woob.capabilities.housing import Query, POSTS_TYPES, ADVERT_TYPES
21 | from woob.tools.test import BackendTest
22 | from woob.tools.capabilities.housing.housing_test import HousingTest
23 |
24 |
25 | class SeLogerTest(BackendTest, HousingTest):
26 | MODULE = 'seloger'
27 |
28 | FIELDS_ALL_HOUSINGS_LIST = [
29 | "id", "type", "advert_type", "house_type", "url", "title", "area",
30 | "utilities", "date", "location", "text"
31 | ]
32 | FIELDS_ANY_HOUSINGS_LIST = [
33 | "cost", # Some posts don't have cost in seloger
34 | "currency", # Same
35 | "photos",
36 | ]
37 | FIELDS_ALL_SINGLE_HOUSING = [
38 | "id", "url", "type", "advert_type", "house_type", "title", "area",
39 | "utilities", "date", "location", "text", "phone", "details"
40 | ]
41 | FIELDS_ANY_SINGLE_HOUSING = [
42 | "cost", # Some posts don't have cost in seloger
43 | "currency", # Same
44 | "photos",
45 | "rooms",
46 | "bedrooms",
47 | "station",
48 | "DPE",
49 | "GES"
50 | ]
51 | DO_NOT_DISTINGUISH_FURNISHED_RENT = True
52 |
53 | def test_seloger_rent(self):
54 | query = Query()
55 | query.area_min = 20
56 | query.cost_max = 1500
57 | query.type = POSTS_TYPES.RENT
58 | query.cities = []
59 | for city in self.backend.search_city('paris'):
60 | city.backend = self.backend.name
61 | query.cities.append(city)
62 | self.check_against_query(query)
63 |
64 | def test_seloger_sale(self):
65 | query = Query()
66 | query.area_min = 20
67 | query.type = POSTS_TYPES.SALE
68 | query.cities = []
69 | for city in self.backend.search_city('paris'):
70 | city.backend = self.backend.name
71 | query.cities.append(city)
72 | self.check_against_query(query)
73 |
74 | def test_seloger_furnished_rent(self):
75 | query = Query()
76 | query.area_min = 20
77 | query.cost_max = 1500
78 | query.type = POSTS_TYPES.FURNISHED_RENT
79 | query.cities = []
80 | for city in self.backend.search_city('paris'):
81 | city.backend = self.backend.name
82 | query.cities.append(city)
83 | self.check_against_query(query)
84 |
85 | def test_seloger_viager(self):
86 | query = Query()
87 | query.type = POSTS_TYPES.VIAGER
88 | query.cities = []
89 | for city in self.backend.search_city('85'):
90 | city.backend = self.backend.name
91 | query.cities.append(city)
92 | self.check_against_query(query)
93 |
94 | def test_seloger_rent_personal(self):
95 | query = Query()
96 | query.area_min = 20
97 | query.cost_max = 1500
98 | query.type = POSTS_TYPES.RENT
99 | query.advert_types = [ADVERT_TYPES.PROFESSIONAL]
100 | query.cities = []
101 | for city in self.backend.search_city('paris'):
102 | city.backend = self.backend.name
103 | query.cities.append(city)
104 | self.check_against_query(query)
105 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## TL;DR
2 |
3 | We have a [code of conduct](CodeOfConduct.md), please make sure to review it
4 | prior to contributing.
5 |
6 | * Branch off `master`.
7 | * One feature per commit.
8 | * In case of changes request, amend your commit.
9 |
10 | You can either open issues / merge requests on [my
11 | Gitlab](https://git.phyks.me/Phyks/flatisfy/) (preferred) or on the [Github
12 | mirror](https://github.com/phyks/flatisfy).
13 |
14 |
15 | ## Useful infos
16 |
17 | * There is a `hooks/pre-commit` file which can be used as a `pre-commit` git
18 | hook to check coding style.
19 | * Python coding style is PEP8. JS coding style is enforced by `eslint`.
20 | * Some useful `npm` scripts are provided (`build:{dev,prod}` /
21 | `watch:{dev,prod}` / `lint`)
22 |
23 |
24 | ## Translating the webapp
25 |
26 | If you want to translate the webapp, just create a new folder in
27 | `flatisfy/web/js_src/i18n` with the short name of your locale (typically, `en`
28 | is for english). Copy the `flatisfy/web/js_src/i18n/en/index.js` file to this
29 | new folder and translate the `messages` strings.
30 |
31 | Then, edit `flatisfy/web/js_src/i18n/index.js` file to include your new
32 | locale.
33 |
34 |
35 | ## How to contribute
36 |
37 | * If you're thinking about a new feature, see if there's already an issue open
38 | about it, or please open one otherwise. This will ensure that everybody is on
39 | track for the feature and willing to see it in Flatisfy.
40 | * One commit per feature.
41 | * Branch off the `master ` branch.
42 | * Check the linting of your code before doing a PR.
43 | * Ideally, your merge-request should be mergeable without any merge commit, that
44 | is, it should be a fast-forward merge. For this to happen, your code needs to
45 | be always rebased onto `master`. Again, this is something nice to have that
46 | I expect from recurring contributors, but not a big deal if you don't do it
47 | otherwise.
48 | * I'll look at it and might ask for a few changes. In this case, please create
49 | new commits. When the final result looks good, I may ask you to squash the
50 | WIP commits into a single one, to maintain the invariant of "one feature, one
51 | commit".
52 |
53 | Thanks!
54 |
55 |
56 | ## Adding support for a new Woob backend
57 |
58 | To enable a new Woob `CapHousing` backend in Flatisfy, you should add it to
59 | the list of available backends in
60 | [flatisfy/fetch.py#L69-70](https://git.phyks.me/Phyks/flatisfy/blob/master/flatisfy/fetch.py#L69-70)
61 | and update the list of `BACKEND_PRECEDENCES` for deduplication in
62 | [flatisfy/filters/duplicates.py#L24-31](https://git.phyks.me/Phyks/flatisfy/blob/master/flatisfy/filters/duplicates.py#L24-31).
63 | Thats' all!
64 |
65 |
66 | ## Adding new data files
67 |
68 | If you want to add new data files, especially for public transportation stops
69 | (to cover more cities), please follow these steps:
70 |
71 | 1. Download and put the **original** file in `flatisfy/data_files`. Please,
72 | use the original data file to ease tracking licenses and be able to still
73 | have a working pipeline, by letting the user download it and place it in
74 | the right place, in case of license conflict.
75 | 2. Mention the added data file and its license in `README.md`, in the
76 | dedicated section.
77 | 3. Write a preprocessing function in `flatisfy/data_files/__init__.py`. You
78 | can have a look at the existing functions for a model.
79 |
80 |
81 | ## Adding new migrations
82 |
83 | If you want to change the database schema, you should create a matching
84 | migration. Here is the way to do it correctly:
85 |
86 | 1. First, edit the `flatisfy/models` files to create / remove the required
87 | fields. If you create a new database from scratch, these are the files
88 | which will be used.
89 | 2. Then, run `alembic revision -m "Some description"` in the root of the git
90 | repo to create a new migration.
91 | 3. Finally, edit the newly created migration file under the `migrations/`
92 | folder to add the required code to alter the database (both upgrade and
93 | downgrade).
94 |
95 |
96 | Thanks!
97 |
--------------------------------------------------------------------------------
/modules/leboncoin/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from woob.tools.test import BackendTest
21 | from woob.tools.value import Value
22 | from woob.capabilities.housing import Query, POSTS_TYPES, ADVERT_TYPES
23 | from woob.tools.capabilities.housing.housing_test import HousingTest
24 |
25 |
26 | class LeboncoinTest(BackendTest, HousingTest):
27 | MODULE = 'leboncoin'
28 |
29 | FIELDS_ALL_HOUSINGS_LIST = [
30 | "id", "type", "advert_type", "url", "title",
31 | "currency", "utilities", "date", "location", "text"
32 | ]
33 | FIELDS_ANY_HOUSINGS_LIST = [
34 | "area",
35 | "cost",
36 | "price_per_meter",
37 | "photos"
38 | ]
39 | FIELDS_ALL_SINGLE_HOUSING = [
40 | "id", "url", "type", "advert_type", "house_type", "title",
41 | "cost", "currency", "utilities", "date", "location", "text",
42 | "rooms", "details"
43 | ]
44 | FIELDS_ANY_SINGLE_HOUSING = [
45 | "area",
46 | "GES",
47 | "DPE",
48 | "photos",
49 | # Don't test phone as leboncoin API is strongly rate-limited
50 | ]
51 |
52 | def setUp(self):
53 | if not self.is_backend_configured():
54 | self.backend.config['advert_type'] = Value(value='a')
55 | self.backend.config['region'] = Value(value='ile_de_france')
56 |
57 | def test_leboncoin_rent(self):
58 | query = Query()
59 | query.area_min = 20
60 | query.cost_max = 1500
61 | query.type = POSTS_TYPES.RENT
62 | query.cities = []
63 | for city in self.backend.search_city('paris'):
64 | city.backend = self.backend.name
65 | query.cities.append(city)
66 | if len(query.cities) == 3:
67 | break
68 | self.check_against_query(query)
69 |
70 | def test_leboncoin_sale(self):
71 | query = Query()
72 | query.area_min = 20
73 | query.type = POSTS_TYPES.SALE
74 | query.cities = []
75 | for city in self.backend.search_city('paris'):
76 | city.backend = self.backend.name
77 | query.cities.append(city)
78 | if len(query.cities) == 3:
79 | break
80 | self.check_against_query(query)
81 |
82 | def test_leboncoin_furnished_rent(self):
83 | query = Query()
84 | query.area_min = 20
85 | query.cost_max = 1500
86 | query.type = POSTS_TYPES.FURNISHED_RENT
87 | query.cities = []
88 | for city in self.backend.search_city('paris'):
89 | city.backend = self.backend.name
90 | query.cities.append(city)
91 | if len(query.cities) == 3:
92 | break
93 | self.check_against_query(query)
94 |
95 | def test_leboncoin_professional(self):
96 | query = Query()
97 | query.area_min = 20
98 | query.cost_max = 900
99 | query.type = POSTS_TYPES.RENT
100 | query.advert_types = [ADVERT_TYPES.PROFESSIONAL]
101 | query.cities = []
102 | for city in self.backend.search_city('paris'):
103 | city.backend = self.backend.name
104 | query.cities.append(city)
105 | self.check_against_query(query)
106 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/components/slider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
99 |
100 |
156 |
--------------------------------------------------------------------------------
/flatisfy/test_files/128358415@seloger.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "128358415@seloger",
3 | "url": "http://www.seloger.com/annonces/achat/maison/rennes-35/128358415.htm?p=",
4 | "title": " 60m\u00b2 - Rennes",
5 | "area": 60,
6 | "cost": 179888,
7 | "price_per_meter": 2998.133333333333333333333333,
8 | "currency": "\u20ac",
9 | "utilities": "",
10 | "date": "2018-01-19T08:46:00",
11 | "location": " Rennes (35000)",
12 | "station": "",
13 | "text": "I@D France - Sarah LECLERC vous propose: Pour les Amoureux de la Pierre, Maison de ville enti\u00e8rement r\u00e9nov\u00e9e avec go\u00fbt et modernit\u00e9, Poutres apparentes dans les 2 chambres, Cuisine am\u00e9nag\u00e9e ouverte sur le salon-salle \u00e0 manger de 30 M 2, Salle de douche, JARDINET et TERRASSE de 95 M 2 (possibilit\u00e9 jardin japonais).. Situ\u00e9e AU COEUR DE LA VILLE, \u00e0 proximit\u00e9 des \u00c9coles, des Commerces et du march\u00e9, tout peut se faire \u00e0 pied.. Ligne de bus \u00e0 proximit\u00e9 (ligne 61).. AUX PORTES DE RENNES (5mn).. Peut se vivre comme un appartement sans les charges de copropri\u00e9t\u00e9 ! BEAUCOUP DE CHARME POUR CE BIEN RARE SUR LE MARCHE !! Honoraires d'agence \u00e0 la charge du vendeur. Information d'affichage \u00e9nerg\u00e9tique sur ce bien: DPE VI indice 0 et GES VI indice 0. La pr\u00e9sente annonce immobili\u00e8re a \u00e9t\u00e9 r\u00e9dig\u00e9e sous la responsabilit\u00e9 \u00e9ditoriale de Mme Sarah LECLERC (ID 27387), Agent Commercial mandataire en immobilier immatricul\u00e9 au Registre Sp\u00e9cial des Agents Commerciaux (RSAC) du Tribunal de Commerce de rennes sous le num\u00e9ro 521558007.",
14 | "phone": null,
15 | "photos": [{
16 | "id": "0j9kfrqnixlcnezpzsgz3g3vnekr6qj8rn7jcv22g.jpg",
17 | "url": "https://v.seloger.com/s/height/800/visuels/0/j/9/k/0j9kfrqnixlcnezpzsgz3g3vnekr6qj8rn7jcv22g.jpg",
18 | "data": null
19 | }, {
20 | "id": "0yqp4d8arum1iy1pk9f1xh1req853dnhutgdjkcoo.jpg",
21 | "url": "https://v.seloger.com/s/height/800/visuels/0/y/q/p/0yqp4d8arum1iy1pk9f1xh1req853dnhutgdjkcoo.jpg",
22 | "data": null
23 | }, {
24 | "id": "10a86qpr9k9wurb8itfnfgzo8eetxs6th2gmiv1o8.jpg",
25 | "url": "https://v.seloger.com/s/height/800/visuels/1/0/a/8/10a86qpr9k9wurb8itfnfgzo8eetxs6th2gmiv1o8.jpg",
26 | "data": null
27 | }, {
28 | "id": "0eybdtrwgscy2dadq05naujq5okeotl5cyfuergvs.jpg",
29 | "url": "https://v.seloger.com/s/height/800/visuels/0/e/y/b/0eybdtrwgscy2dadq05naujq5okeotl5cyfuergvs.jpg",
30 | "data": null
31 | }, {
32 | "id": "0maihs9wfff2xl3plqtq254n44gkaxlvejyrtnbqw.jpg",
33 | "url": "https://v.seloger.com/s/height/800/visuels/0/m/a/i/0maihs9wfff2xl3plqtq254n44gkaxlvejyrtnbqw.jpg",
34 | "data": null
35 | }, {
36 | "id": "0cjgak7htwwtsl4to31rqqmyg5a73h6vwzserq2wo.jpg",
37 | "url": "https://v.seloger.com/s/height/800/visuels/0/c/j/g/0cjgak7htwwtsl4to31rqqmyg5a73h6vwzserq2wo.jpg",
38 | "data": null
39 | }, {
40 | "id": "102tkunk4f87ksovtm7x6u1awoz65it97nabbx9a0.jpg",
41 | "url": "https://v.seloger.com/s/height/800/visuels/1/0/2/t/102tkunk4f87ksovtm7x6u1awoz65it97nabbx9a0.jpg",
42 | "data": null
43 | }, {
44 | "id": "1kd6jjp93vv5wv5dw8964n7t823luy8jk3m4obkfs.jpg",
45 | "url": "https://v.seloger.com/s/height/800/visuels/1/k/d/6/1kd6jjp93vv5wv5dw8964n7t823luy8jk3m4obkfs.jpg",
46 | "data": null
47 | }, {
48 | "id": "052a19zndeojbs4px73q8ns94g1uxi0exxqyltpo8.jpg",
49 | "url": "https://v.seloger.com/s/height/800/visuels/0/5/2/a/052a19zndeojbs4px73q8ns94g1uxi0exxqyltpo8.jpg",
50 | "data": null
51 | }],
52 | "rooms": 3,
53 | "bedrooms": 2,
54 | "details": {
55 | "Cuisine": "am\u00e9ricaine \u00e9quip\u00e9e",
56 | "Pi\u00e8ces": "3",
57 | "Etage": "1",
58 | "Reference": "488187",
59 | "Chambres": "2",
60 | "Chauffage": "\u00e9lectrique radiateur",
61 | "Terrain": "95 m\u00b2",
62 | "Surface": "60 m\u00b2",
63 | "Terrasse": "1",
64 | "Ann\u00e9e de construction": "1870",
65 | "Salle \u00c0 Manger": "",
66 | "Salle de s\u00e9jour": "22 m\u00b2"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/modules/pap/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2012 Romain Bignon
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from woob.capabilities.housing import Query, POSTS_TYPES, ADVERT_TYPES
21 | from woob.tools.test import BackendTest
22 | from woob.tools.capabilities.housing.housing_test import HousingTest
23 |
24 |
25 | class PapTest(BackendTest, HousingTest):
26 | MODULE = 'pap'
27 |
28 | FIELDS_ALL_HOUSINGS_LIST = [
29 | "id", "type", "advert_type", "house_type", "url", "title", "area",
30 | "cost", "currency", "utilities", "location", "text"
31 | ]
32 | FIELDS_ANY_HOUSINGS_LIST = [
33 | "photos",
34 | "station",
35 | ]
36 | FIELDS_ALL_SINGLE_HOUSING = [
37 | "id", "url", "type", "advert_type", "house_type", "title", "area",
38 | "cost", "currency", "utilities", "date", "location", "text",
39 | "phone"
40 | ]
41 | FIELDS_ANY_SINGLE_HOUSING = [
42 | "photos",
43 | "rooms",
44 | "bedrooms",
45 | "station"
46 | ]
47 |
48 | def test_pap_rent(self):
49 | query = Query()
50 | query.area_min = 20
51 | query.cost_max = 1500
52 | query.type = POSTS_TYPES.RENT
53 | query.cities = []
54 | for city in self.backend.search_city('paris'):
55 | city.backend = self.backend.name
56 | query.cities.append(city)
57 | self.check_against_query(query)
58 |
59 | def test_pap_sale(self):
60 | query = Query()
61 | query.area_min = 20
62 | query.type = POSTS_TYPES.SALE
63 | query.cities = []
64 | for city in self.backend.search_city('paris'):
65 | city.backend = self.backend.name
66 | query.cities.append(city)
67 | self.check_against_query(query)
68 |
69 | def test_pap_furnished_rent(self):
70 | query = Query()
71 | query.area_min = 20
72 | query.cost_max = 1500
73 | query.type = POSTS_TYPES.FURNISHED_RENT
74 | query.cities = []
75 | for city in self.backend.search_city('paris'):
76 | city.backend = self.backend.name
77 | query.cities.append(city)
78 | self.check_against_query(query)
79 |
80 | def test_pap_viager(self):
81 | query = Query()
82 | query.type = POSTS_TYPES.VIAGER
83 | query.cities = []
84 | for city in self.backend.search_city('paris'):
85 | city.backend = self.backend.name
86 | query.cities.append(city)
87 | # Remove rooms from the tested fields as viager never have them
88 | self.FIELDS_ANY_HOUSINGS_LIST = [
89 | "photos",
90 | "station",
91 | "bedrooms"
92 | ]
93 | self.FIELDS_ANY_SINGLE_HOUSING = [
94 | "photos",
95 | "bedrooms",
96 | "station"
97 | ]
98 | self.check_against_query(query)
99 |
100 | def test_pap_professional(self):
101 | query = Query()
102 | query.area_min = 20
103 | query.cost_max = 900
104 | query.type = POSTS_TYPES.RENT
105 | query.advert_types = [ADVERT_TYPES.PROFESSIONAL]
106 | query.cities = []
107 | for city in self.backend.search_city('paris'):
108 | city.backend = self.backend.name
109 | query.cities.append(city)
110 |
111 | results = list(self.backend.search_housings(query))
112 | self.assertEqual(len(results), 0)
113 |
--------------------------------------------------------------------------------
/modules/logicimmo/test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 | from woob.capabilities.housing import Query, POSTS_TYPES, ADVERT_TYPES
21 | from woob.tools.test import BackendTest
22 | from woob.tools.capabilities.housing.housing_test import HousingTest
23 |
24 |
25 | class LogicimmoTest(BackendTest, HousingTest):
26 | MODULE = 'logicimmo'
27 |
28 | FIELDS_ALL_HOUSINGS_LIST = [
29 | "id", "type", "advert_type", "house_type", "url", "title", "area",
30 | "cost", "currency", "utilities", "date", "location", "text",
31 | "details", "rooms"
32 | ]
33 | FIELDS_ANY_HOUSINGS_LIST = [
34 | "photos",
35 | ]
36 | FIELDS_ALL_SINGLE_HOUSING = [
37 | "id", "url", "type", "advert_type", "house_type", "title", "area",
38 | "cost", "currency", "utilities", "date", "location", "text",
39 | "phone", "details"
40 | ]
41 | FIELDS_ANY_SINGLE_HOUSING = [
42 | "photos",
43 | "station",
44 | "rooms",
45 | "phone",
46 | "DPE",
47 | "GES"
48 | ]
49 | DO_NOT_DISTINGUISH_FURNISHED_RENT = True
50 |
51 | def test_logicimmo_rent(self):
52 | query = Query()
53 | query.area_min = 20
54 | query.cost_max = 1500
55 | query.type = POSTS_TYPES.RENT
56 | query.cities = []
57 | for city in self.backend.search_city('paris'):
58 | city.backend = self.backend.name
59 | query.cities.append(city)
60 | if len(query.cities) == 3:
61 | break
62 | self.check_against_query(query)
63 |
64 | def test_logicimmo_sale(self):
65 | query = Query()
66 | query.area_min = 20
67 | query.type = POSTS_TYPES.SALE
68 | query.cities = []
69 | for city in self.backend.search_city('paris'):
70 | city.backend = self.backend.name
71 | query.cities.append(city)
72 | if len(query.cities) == 3:
73 | break
74 | self.check_against_query(query)
75 |
76 | def test_logicimmo_furnished_rent(self):
77 | query = Query()
78 | query.area_min = 20
79 | query.cost_max = 1500
80 | query.type = POSTS_TYPES.FURNISHED_RENT
81 | query.cities = []
82 | for city in self.backend.search_city('paris'):
83 | city.backend = self.backend.name
84 | query.cities.append(city)
85 | if len(query.cities) == 3:
86 | break
87 | self.check_against_query(query)
88 |
89 | def test_logicimmo_viager(self):
90 | query = Query()
91 | query.type = POSTS_TYPES.VIAGER
92 | query.cities = []
93 | for city in self.backend.search_city('paris'):
94 | city.backend = self.backend.name
95 | query.cities.append(city)
96 | if len(query.cities) == 3:
97 | break
98 | self.check_against_query(query)
99 |
100 | def test_logicimmo_personal(self):
101 | query = Query()
102 | query.area_min = 20
103 | query.cost_max = 900
104 | query.type = POSTS_TYPES.RENT
105 | query.advert_types = [ADVERT_TYPES.PERSONAL]
106 | query.cities = []
107 | for city in self.backend.search_city('paris'):
108 | city.backend = self.backend.name
109 | query.cities.append(city)
110 |
111 | results = list(self.backend.search_housings(query))
112 | self.assertEqual(len(results), 0)
113 |
--------------------------------------------------------------------------------
/modules/logicimmo/browser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Copyright(C) 2014 Bezleputh
4 | #
5 | # This file is part of a woob module.
6 | #
7 | # This woob module is free software: you can redistribute it and/or modify
8 | # it under the terms of the GNU Affero General Public License as published by
9 | # the Free Software Foundation, either version 3 of the License, or
10 | # (at your option) any later version.
11 | #
12 | # This woob module is distributed in the hope that it will be useful,
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | # GNU Affero General Public License for more details.
16 | #
17 | # You should have received a copy of the GNU Affero General Public License
18 | # along with this woob module. If not, see .
19 |
20 |
21 | from woob.browser import PagesBrowser, URL
22 | from woob.browser.profiles import Firefox
23 | from woob.capabilities.housing import (TypeNotSupported, POSTS_TYPES,
24 | HOUSE_TYPES)
25 | from .pages import CitiesPage, SearchPage, HousingPage, PhonePage
26 |
27 |
28 | class LogicimmoBrowser(PagesBrowser):
29 | BASEURL = 'https://www.logic-immo.com/'
30 | PROFILE = Firefox()
31 | city = URL('asset/t9/getLocalityT9.php\?site=fr&lang=fr&json=%22(?P.*)%22',
32 | CitiesPage)
33 | search = URL('(?Plocation-immobilier|vente-immobilier|recherche-colocation)-(?P.*)/options/(?P.*)', SearchPage)
34 | housing = URL('detail-(?P<_id>.*).htm', HousingPage)
35 | phone = URL('(?P.*)', PhonePage)
36 |
37 | TYPES = {POSTS_TYPES.RENT: 'location-immobilier',
38 | POSTS_TYPES.SALE: 'vente-immobilier',
39 | POSTS_TYPES.SHARING: 'recherche-colocation',
40 | POSTS_TYPES.FURNISHED_RENT: 'location-immobilier',
41 | POSTS_TYPES.VIAGER: 'vente-immobilier'}
42 |
43 | RET = {HOUSE_TYPES.HOUSE: '2',
44 | HOUSE_TYPES.APART: '1',
45 | HOUSE_TYPES.LAND: '3',
46 | HOUSE_TYPES.PARKING: '10',
47 | HOUSE_TYPES.OTHER: '14'}
48 |
49 | def __init__(self, *args, **kwargs):
50 | super(LogicimmoBrowser, self).__init__(*args, **kwargs)
51 | self.session.headers['X-Requested-With'] = 'XMLHttpRequest'
52 |
53 | def get_cities(self, pattern):
54 | if pattern:
55 | return self.city.go(pattern=pattern).get_cities()
56 |
57 | def search_housings(self, type, cities, nb_rooms, area_min, area_max, cost_min, cost_max, house_types):
58 | if type not in self.TYPES:
59 | raise TypeNotSupported()
60 |
61 | options = []
62 |
63 | ret = []
64 | if type == POSTS_TYPES.VIAGER:
65 | ret = ['15']
66 | else:
67 | for house_type in house_types:
68 | if house_type in self.RET:
69 | ret.append(self.RET.get(house_type))
70 |
71 | if len(ret):
72 | options.append('groupprptypesids=%s' % ','.join(ret))
73 |
74 | if type == POSTS_TYPES.FURNISHED_RENT:
75 | options.append('searchoptions=4')
76 |
77 | options.append('pricemin=%s' % (cost_min if cost_min else '0'))
78 |
79 | if cost_max:
80 | options.append('pricemax=%s' % cost_max)
81 |
82 | options.append('areamin=%s' % (area_min if area_min else '0'))
83 |
84 | if area_max:
85 | options.append('areamax=%s' % area_max)
86 |
87 | if nb_rooms:
88 | if type == POSTS_TYPES.SHARING:
89 | options.append('nbbedrooms=%s' % ','.join([str(i) for i in range(nb_rooms, 7)]))
90 | else:
91 | options.append('nbrooms=%s' % ','.join([str(i) for i in range(nb_rooms, 7)]))
92 |
93 | self.search.go(type=self.TYPES.get(type, 'location-immobilier'),
94 | cities=cities,
95 | options='/'.join(options))
96 |
97 | if type == POSTS_TYPES.SHARING:
98 | return self.page.iter_sharing()
99 |
100 | return self.page.iter_housings(query_type=type)
101 |
102 | def get_housing(self, _id, housing=None):
103 | return self.housing.go(_id=_id).get_housing(obj=housing)
104 |
105 | def get_phone(self, _id):
106 | if _id.startswith('location') or _id.startswith('vente'):
107 | urlcontact, params = self.housing.stay_or_go(_id=_id).get_phone_url_datas()
108 | return self.phone.go(urlcontact=urlcontact, params=params).get_phone()
109 |
--------------------------------------------------------------------------------
/flatisfy/filters/cache.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | Caching function for pictures.
4 | """
5 |
6 | from __future__ import absolute_import, print_function, unicode_literals
7 |
8 | import collections
9 | import hashlib
10 | import os
11 | import requests
12 | import logging
13 | from io import BytesIO
14 |
15 | import PIL.Image
16 |
17 | LOGGER = logging.getLogger(__name__)
18 |
19 |
20 | class MemoryCache(object):
21 | """
22 | A cache in memory.
23 | """
24 |
25 | @staticmethod
26 | def on_miss(key):
27 | """
28 | Method to be called whenever an object is requested from the cache but
29 | was not already cached. Typically, make a HTTP query to fetch it.
30 |
31 | :param key: Key of the requested object.
32 | :return: The object content.
33 | """
34 | raise NotImplementedError
35 |
36 | def __init__(self):
37 | self.hits = 0
38 | self.misses = 0
39 | self.map = collections.OrderedDict()
40 |
41 | def get(self, key):
42 | """
43 | Get an element from cache. Eventually call ``on_miss`` if the item is
44 | not already cached.
45 |
46 | :param key: Key of the element to retrieve.
47 | :return: Requested element.
48 | """
49 | cached = self.map.get(key, None)
50 | if cached is not None:
51 | self.hits += 1
52 | return cached
53 |
54 | item = self.map[key] = self.on_miss(key)
55 | self.misses += 1
56 | return item
57 |
58 | def total(self):
59 | """
60 | Get the total number of calls (with hits to the cache, or miss and
61 | fetching with ``on_miss``) to the cache.
62 |
63 | :return: Total number of item accessing.
64 | """
65 | return self.hits + self.misses
66 |
67 | def hit_rate(self):
68 | """
69 | Get the hit rate, that is the rate at which we requested an item which
70 | was already in the cache.
71 |
72 | :return: The hit rate, in percents.
73 | """
74 | assert self.total() > 0
75 | return 100 * self.hits // self.total()
76 |
77 | def miss_rate(self):
78 | """
79 | Get the miss rate, that is the rate at which we requested an item which
80 | was not already in the cache.
81 |
82 | :return: The miss rate, in percents.
83 | """
84 | assert self.total() > 0
85 | return 100 * self.misses // self.total()
86 |
87 |
88 | class ImageCache(MemoryCache):
89 | """
90 | A cache for images, stored in memory.
91 | """
92 |
93 | @staticmethod
94 | def compute_filename(url):
95 | """
96 | Compute filename (hash of the URL) for the cached image.
97 |
98 | :param url: The URL of the image.
99 | :return: The filename, with its extension.
100 | """
101 | # Always store as JPEG
102 | return "%s.jpg" % hashlib.sha1(url.encode("utf-8")).hexdigest()
103 |
104 | def on_miss(self, url):
105 | """
106 | Helper to actually retrieve photos if not already cached.
107 | """
108 | # If two many items in the cache, pop one
109 | if len(self.map.keys()) > self.max_items:
110 | self.map.popitem(last=False)
111 |
112 | if url.endswith(".svg"):
113 | # Skip SVG photo which are unsupported and unlikely to be relevant
114 | return None
115 |
116 | filepath = None
117 | # Try to load from local folder
118 | if self.storage_dir:
119 | filepath = os.path.join(self.storage_dir, self.compute_filename(url))
120 | if os.path.isfile(filepath):
121 | return PIL.Image.open(filepath)
122 | # Otherwise, fetch it
123 | try:
124 | LOGGER.debug(f"Download photo from {url} to {filepath}")
125 | req = requests.get(url)
126 | req.raise_for_status()
127 | image = PIL.Image.open(BytesIO(req.content))
128 | if filepath:
129 | image.save(filepath, format=image.format)
130 | return image
131 | except (requests.HTTPError, IOError) as exc:
132 | LOGGER.info(f"Download photo from {url} failed: {exc}")
133 | return None
134 |
135 | def __init__(self, max_items=200, storage_dir=None):
136 | """
137 | :param max_items: Max number of items in the cache, to prevent Out Of
138 | Memory errors.
139 | :param storage_dir: Directory in which images should be stored.
140 | """
141 | self.max_items = max_items
142 | self.storage_dir = storage_dir
143 | if self.storage_dir and not os.path.isdir(self.storage_dir):
144 | os.makedirs(self.storage_dir)
145 | super(ImageCache, self).__init__()
146 |
--------------------------------------------------------------------------------
/flatisfy/web/js_src/components/flatsmap.vue:
--------------------------------------------------------------------------------
1 |
2 |