From 67cd18991ede6dc19371020586e93eed90310948 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Mon, 21 Oct 2024 16:47:04 +0200 Subject: [PATCH 001/107] chore: dynamic docker project name, fix db port custom --- docker/.env.tpl | 4 +++- docker/docker-compose.yml | 4 ++-- docker/docker.sh | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docker/.env.tpl b/docker/.env.tpl index d7f7bbc7..53367384 100644 --- a/docker/.env.tpl +++ b/docker/.env.tpl @@ -1,3 +1,5 @@ +PROJECT=geopaysages + PROXY_HTTP_PORT=80 # HTTPS with Traefik and LetsEncrypt : PROXY_HTTPS_PORT=443 @@ -25,7 +27,7 @@ CODE_APPLICATION=GP DEBUG=0 ADMIN_ENV_DEV=0 DB_ADDRESS=db -DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_ADDRESS}:${DB_PORT}/${DB_NAME} +DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_ADDRESS}:5432/${DB_NAME} CUSTOM_PATH=../custom # PROXY_API_PORT=8081 \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6a559a29..ed239926 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -77,9 +77,9 @@ services: image: ${DB_IMAGE:-ghcr.io/pnx-si/geopaysages_db:latest} restart: always ports: - - "${DB_PORT}:${DB_PORT}" + - "${DB_PORT}:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME} -p ${DB_PORT} -h 127.0.0.1"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME} -p 5432 -h 127.0.0.1"] interval: 5s timeout: 5s retries: 3 diff --git a/docker/docker.sh b/docker/docker.sh index 4dbd88a6..6a04c8b6 100755 --- a/docker/docker.sh +++ b/docker/docker.sh @@ -56,7 +56,7 @@ then fi if [ "$HTTPS_IN_PROXY" == "1" ]; then - ${launch_compose} --project-name="geopaysages" -f ./docker/docker-compose.yml -f ./docker/docker-compose.https.yml "$@" + ${launch_compose} --project-name="${PROJECT}" -f ./docker/docker-compose.yml -f ./docker/docker-compose.https.yml "$@" else - ${launch_compose} --project-name="geopaysages" --project-directory=./docker "$@" + ${launch_compose} --project-name="${PROJECT}" --project-directory=./docker "$@" fi \ No newline at end of file From 0dc687a617a9a04174526e93bdd32889b571a4f6 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Mon, 21 Oct 2024 18:07:21 +0200 Subject: [PATCH 002/107] fix: home map padding to ensure markers visibility --- backend/static/js/home_mono_obs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/static/js/home_mono_obs.js b/backend/static/js/home_mono_obs.js index 63a6044c..a1af7875 100644 --- a/backend/static/js/home_mono_obs.js +++ b/backend/static/js/home_mono_obs.js @@ -44,7 +44,7 @@ geopsg.initHomeMono = (options) => { map.fitBounds([ [lats[0], lons[0]], [lats[lats.length - 1], lons[lons.length - 1]], - ]); + ], { paddingTopLeft: [40, 50], paddingBottomRight: [40, 30] }); }); function onResize() { From cce8d309a52dc012a4a0aefe5dbcbab97473ae47 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Mon, 21 Oct 2024 18:17:50 +0200 Subject: [PATCH 003/107] fix: route /api/me --- backend/api.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/api.py b/backend/api.py index 53146fbc..85437816 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,5 +1,5 @@ from flask import Flask, request, Blueprint, Response, jsonify, abort, Response, current_app -from flask_login import login_required +from flask_login import login_required, current_user from werkzeug.exceptions import NotFound from werkzeug.wsgi import FileWrapper @@ -254,13 +254,14 @@ def returnAllUsers(id_app): # use in the front at each refresh ... but why ? @api.route('/api/me/', methods=['GET']) @fnauth.check_auth(2) -def returnCurrentUser(id_role=None): - current_user = AppUser.query.filter_by( +def returnCurrentUser(): + id_role = current_user.id_role + user_data = AppUser.query.filter_by( id_role=id_role ).all() - if not current_user: + if not user_data: raise NotFound(f"No User with id {id_role}") - return jsonify([d.as_dict() for d in current_user]) + return jsonify([d.as_dict() for d in user_data]) @api.route('/api/site/', methods=['DELETE']) From 2228b8a7cb9143b5c426387378df502eaa433db6 Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Fri, 25 Oct 2024 10:25:36 +0200 Subject: [PATCH 004/107] style: add workflows for lint backend Reviewed-by: andriacap --- .github/workflows/lint.yml | 25 +++++++++++++++++++++++++ backend/requirements.txt | 1 + 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..43ed0f91 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,25 @@ +name: Lint Code with Black + +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.9" # Spécifie la version de Python que tu utilises + + - name: Install Black + run: | + python -m pip install --upgrade pip + pip install black==24.10.0 + + - name: Run Black + run: black --check ./backend diff --git a/backend/requirements.txt b/backend/requirements.txt index 2625da56..a9e30642 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -25,3 +25,4 @@ six==1.15.0 SQLAlchemy==1.4.39 Werkzeug==2.2.2 gunicorn==20.1.0 +black==24.10.0 From e60c2ce770a25a0b8bf039db3d7988effddb184f Mon Sep 17 00:00:00 2001 From: Andria Capai Date: Fri, 25 Oct 2024 10:30:50 +0200 Subject: [PATCH 005/107] style: apply black Reviewed-by: andriacap --- .github/workflows/lint.yml | 2 +- backend/api.py | 333 ++++++++++-------- backend/app.py | 40 ++- backend/config.py | 10 +- backend/env.py | 5 +- backend/migrations/env.py | 44 +-- .../33990994eeae_observatory_images.py | 39 +- ...bd35bb30_observatory_photo_to_thumbnail.py | 21 +- .../6443d436740a_site_main_observatory.py | 25 +- .../versions/ba8ff1826771_some_icons.py | 21 +- .../d7b6052f4dad_photo_id_observatory.py | 25 +- .../versions/d8d449eefa0f_observatory.py | 73 ++-- backend/migrations/versions/f545b345135c_.py | 13 +- .../ffd0e83f3c4c_initial_migration.py | 7 +- backend/models.py | 257 ++++++++------ backend/routes.py | 199 +++++++---- backend/utils.py | 308 +++++++++------- 17 files changed, 855 insertions(+), 567 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 43ed0f91..d597d856 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,4 +22,4 @@ jobs: pip install black==24.10.0 - name: Run Black - run: black --check ./backend + run: black --check --exclude "backend/custom_app.py" ./backend diff --git a/backend/api.py b/backend/api.py index 85437816..b60f7946 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,4 +1,13 @@ -from flask import Flask, request, Blueprint, Response, jsonify, abort, Response, current_app +from flask import ( + Flask, + request, + Blueprint, + Response, + jsonify, + abort, + Response, + current_app, +) from flask_login import login_required, current_user from werkzeug.exceptions import NotFound from werkzeug.wsgi import FileWrapper @@ -16,7 +25,7 @@ from env import db -api = Blueprint('api', __name__) +api = Blueprint("api", __name__) photo_schema = models.TPhotoSchema(many=True) observatory_schema_full = models.ObservatorySchemaFull(many=False) @@ -28,23 +37,28 @@ corThemeStheme_Schema = models.CorThemeSthemeSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) -@api.route('/api/thumbor/presets//', methods=['GET']) + +@api.route("/api/thumbor/presets//", methods=["GET"]) def thumborPreset(name, filename): presets = { - 'noxl': 'fit-in/5000x5000/filters:no_upscale():quality(90)', - '50x50': '50x50', - '100x100': '100x100', - '150x150': '150x150', - '200x150': '200x150', - '200x200': '200x200', + "noxl": "fit-in/5000x5000/filters:no_upscale():quality(90)", + "50x50": "50x50", + "100x100": "100x100", + "150x150": "150x150", + "200x150": "200x150", + "200x200": "200x200", } preset = presets.get(name) if not preset: abort(404) - - url = preset + '/' + urllib.parse.quote(f'http://backend/static/upload/images/{filename}', safe='') + + url = ( + preset + + "/" + + urllib.parse.quote(f"http://backend/static/upload/images/{filename}", safe="") + ) signature = utils.getThumborSignature(url) - response = requests.get(f'http://thumbor:8000/{signature}/{url}') + response = requests.get(f"http://thumbor:8000/{signature}/{url}") if response.status_code != 200: abort(response.status_code) @@ -54,22 +68,24 @@ def thumborPreset(name, filename): w = FileWrapper(b) return Response(w, mimetype=type[0]) -@api.route('/api/conf', methods=['GET']) + +@api.route("/api/conf", methods=["GET"]) @fnauth.check_auth(2) def returnDdConf(): dbconf = utils.getDbConf() - + return jsonify(dbconf) -@api.route('/api/observatories', methods=['GET']) + +@api.route("/api/observatories", methods=["GET"]) def returnAllObservatories(): - get_all = models.Observatory.query.order_by('title').all() + get_all = models.Observatory.query.order_by("title").all() items = observatories_schema.dump(get_all) - + return jsonify(items) -@api.route('/api/observatories', methods=['POST']) +@api.route("/api/observatories", methods=["POST"]) @fnauth.check_auth(2) def postObservatory(): try: @@ -80,13 +96,13 @@ def postObservatory(): except Exception as exception: print(exception) return str(exception), 400 - + db.session.refresh(db_obj) resp = observatory_schema_full.dump(db_obj) return jsonify(resp) -@api.route('/api/observatories/', methods=['GET']) +@api.route("/api/observatories/", methods=["GET"]) def returnObservatoryById(id): row = models.Observatory.query.filter_by(id=id).first() if not row: @@ -95,7 +111,7 @@ def returnObservatoryById(id): return jsonify(dict) -@api.route('/api/observatories/', methods=['PATCH']) +@api.route("/api/observatories/", methods=["PATCH"]) @fnauth.check_auth(2) def patchObservatory(id): try: @@ -112,28 +128,26 @@ def patchObservatory(id): return jsonify(dict) -@api.route('/api/observatories//image', methods=['PATCH']) +@api.route("/api/observatories//image", methods=["PATCH"]) @fnauth.check_auth(2) def patchObservatoryImage(id): - field = request.form.get('field') - if field not in ['thumbnail', 'logo']: + field = request.form.get("field") + if field not in ["thumbnail", "logo"]: return "Invalid field value: " + str(field), 400 - image = request.files.get('image') + image = request.files.get("image") if not image: return "Missing field: image", 400 rows = models.Observatory.query.filter_by(id=id) if not rows.count(): abort(404) - + dicts = observatories_schema.dump(rows) - base_path = '/app/static/upload/images/' - + base_path = "/app/static/upload/images/" + _, ext = os.path.splitext(image.filename) - filename = 'observatory-' + str(id) + '-' + field + '-' + utils.getRandStr(4) + ext + filename = "observatory-" + str(id) + "-" + field + "-" + utils.getRandStr(4) + ext image.save(os.path.join(base_path + filename)) - rows.update({ - field: filename - }) + rows.update({field: filename}) db.session.commit() if dicts[0][field]: @@ -142,30 +156,31 @@ def patchObservatoryImage(id): except Exception as exception: pass - return jsonify({ - 'filename': filename - }), 200 + return jsonify({"filename": filename}), 200 -@api.route('/api/sites', methods=['GET']) +@api.route("/api/sites", methods=["GET"]) def returnAllSites(): dbconf = utils.getDbConf() - get_all_sites = models.TSite.query.order_by(dbconf['default_sort_sites']).all() + get_all_sites = models.TSite.query.order_by(dbconf["default_sort_sites"]).all() sites = site_schema.dump(get_all_sites) for site in sites: if len(site.get("t_photos")) > 0: - if site.get('main_photo') == None : + if site.get("main_photo") == None: first_photo = site.get("t_photos") main_photo = models.TPhoto.query.filter_by( id_photo=first_photo[0] ).one_or_none() else: main_photo = models.TPhoto.query.filter_by( - id_photo=site.get('main_photo')).one_or_none() + id_photo=site.get("main_photo") + ).one_or_none() if main_photo: photo_schema = models.TPhotoSchema() main_photo = photo_schema.dump(main_photo) - site['main_photo'] = main_photo.get("path_file_photo") #utils.getThumbnail(main_photo).get('output_name') + site["main_photo"] = main_photo.get( + "path_file_photo" + ) # utils.getThumbnail(main_photo).get('output_name') else: site["main_photo"] = "no_photo" @@ -175,118 +190,123 @@ def returnAllSites(): return jsonify(sites) -@api.route('/api/site/', methods=['GET']) +@api.route("/api/site/", methods=["GET"]) def returnSiteById(id_site): get_site_by_id = models.TSite.query.filter_by(id_site=id_site) site = site_schema.dump(get_site_by_id) - get_photos_by_site = models.TPhoto.query.order_by( - 'filter_date').filter_by(id_site=id_site).all() + get_photos_by_site = ( + models.TPhoto.query.order_by("filter_date").filter_by(id_site=id_site).all() + ) dump_photos = photo_schema.dump(get_photos_by_site) - cor_sthemes_themes = site[0].get('cor_site_stheme_themes') + cor_sthemes_themes = site[0].get("cor_site_stheme_themes") cor_list = [] themes_list = [] subthemes_list = [] for cor in cor_sthemes_themes: - cor_list.append(cor.get('id_stheme_theme')) + cor_list.append(cor.get("id_stheme_theme")) query = models.CorSthemeTheme.query.filter( - models.CorSthemeTheme.id_stheme_theme.in_(cor_list)) + models.CorSthemeTheme.id_stheme_theme.in_(cor_list) + ) themes_sthemes = themes_sthemes_schema.dump(query) for item in themes_sthemes: - if item.get('dico_theme').get('id_theme') not in themes_list: - themes_list.append(item.get('dico_theme').get('id_theme')) - if item.get('dico_stheme').get('id_stheme') not in subthemes_list: - subthemes_list.append(item.get('dico_stheme').get('id_stheme')) + if item.get("dico_theme").get("id_theme") not in themes_list: + themes_list.append(item.get("dico_theme").get("id_theme")) + if item.get("dico_stheme").get("id_stheme") not in subthemes_list: + subthemes_list.append(item.get("dico_stheme").get("id_stheme")) - site[0]['themes'] = themes_list - site[0]['subthemes'] = subthemes_list + site[0]["themes"] = themes_list + site[0]["subthemes"] = subthemes_list photos = dump_photos return jsonify(site=site, photos=photos), 200 -@api.route('/api/gallery', methods=['GET']) +@api.route("/api/gallery", methods=["GET"]) def gallery(): - get_photos = models.TPhoto.query.order_by('id_site').all() + get_photos = models.TPhoto.query.order_by("id_site").all() dump_photos = photo_schema.dump(get_photos) return jsonify(dump_photos), 200 -@api.route('/api/themes', methods=['GET']) +@api.route("/api/themes", methods=["GET"]) def returnAllThemes(): get_all_themes = models.DicoTheme.query.all() themes = themes_schema.dump(get_all_themes) return jsonify(themes), 200 -@api.route('/api/subThemes', methods=['GET']) +@api.route("/api/subThemes", methods=["GET"]) def returnAllSubthemes(): get_all_subthemes = models.DicoStheme.query.all() subthemes = subthemes_schema.dump(get_all_subthemes) for sub in subthemes: themes_of_subthemes = [] - for item in sub.get('cor_stheme_themes'): - themes_of_subthemes.append(item.get('id_theme')) - sub['themes'] = themes_of_subthemes - del sub['cor_stheme_themes'] + for item in sub.get("cor_stheme_themes"): + themes_of_subthemes.append(item.get("id_theme")) + sub["themes"] = themes_of_subthemes + del sub["cor_stheme_themes"] return jsonify(subthemes), 200 -@api.route('/api/licences', methods=['GET']) +@api.route("/api/licences", methods=["GET"]) def returnAllLicences(): get_all_licences = models.DicoLicencePhoto.query.all() licences = licences_schema.dump(get_all_licences) return jsonify(licences), 200 -@api.route('/api/users/', methods=['GET']) + +@api.route("/api/users/", methods=["GET"]) @login_required def returnAllUsers(id_app): - a = Application.query.filter_by(code_application=current_app.config["CODE_APPLICATION"]).one() - all_users = AppUser.query.filter_by( - id_application=id_app).all() + a = Application.query.filter_by( + code_application=current_app.config["CODE_APPLICATION"] + ).one() + all_users = AppUser.query.filter_by(id_application=id_app).all() - return jsonify([u.as_dict() for u in all_users if u.id_application == a.id_application]) + return jsonify( + [u.as_dict() for u in all_users if u.id_application == a.id_application] + ) -# TODO : remove this view ! + +# TODO : remove this view ! # use in the front at each refresh ... but why ? -@api.route('/api/me/', methods=['GET']) +@api.route("/api/me/", methods=["GET"]) @fnauth.check_auth(2) def returnCurrentUser(): id_role = current_user.id_role - user_data = AppUser.query.filter_by( - id_role=id_role - ).all() + user_data = AppUser.query.filter_by(id_role=id_role).all() if not user_data: raise NotFound(f"No User with id {id_role}") return jsonify([d.as_dict() for d in user_data]) -@api.route('/api/site/', methods=['DELETE']) +@api.route("/api/site/", methods=["DELETE"]) @fnauth.check_auth(6) def deleteSite(id_site): - base_path = '/app/static/upload/images/' + base_path = "/app/static/upload/images/" models.CorSiteSthemeTheme.query.filter_by(id_site=id_site).delete() photos = models.TPhoto.query.filter_by(id_site=id_site).all() photos = photo_schema.dump(photos) models.TPhoto.query.filter_by(id_site=id_site).delete() site = models.TSite.query.filter_by(id_site=id_site).delete() for photo in photos: - photo_name = photo.get('path_file_photo') + photo_name = photo.get("path_file_photo") for fileName in os.listdir(base_path): if fileName.endswith(photo_name): os.remove(base_path + fileName) db.session.commit() if site: - return jsonify('site has been deleted'), 200 + return jsonify("site has been deleted"), 200 else: - return jsonify('error'), 400 + return jsonify("error"), 400 -@api.route('/api/addSite', methods=['POST']) +@api.route("/api/addSite", methods=["POST"]) @fnauth.check_auth(2) def add_site(): data = dict(request.get_json()) @@ -297,161 +317,172 @@ def add_site(): return jsonify(id_site=site.id_site), 200 -@api.route('/api/updateSite', methods=['PATCH']) +@api.route("/api/updateSite", methods=["PATCH"]) @fnauth.check_auth(2) def update_site(): site = request.get_json() - models.CorSiteSthemeTheme.query.filter_by( - id_site=site.get('id_site')).delete() - models.TSite.query.filter_by(id_site=site.get('id_site')).update(site) + models.CorSiteSthemeTheme.query.filter_by(id_site=site.get("id_site")).delete() + models.TSite.query.filter_by(id_site=site.get("id_site")).update(site) db.session.commit() - return jsonify('site updated successfully'), 200 + return jsonify("site updated successfully"), 200 -@api.route('/api/addThemes', methods=['POST']) +@api.route("/api/addThemes", methods=["POST"]) @fnauth.check_auth(2) def add_cor_site_theme_stheme(): - data = request.get_json().get('data') + data = request.get_json().get("data") for d in data: get_id_stheme_theme = models.CorSthemeTheme.query.filter_by( - id_theme=d.get('id_theme'), id_stheme=d.get('id_stheme')).all() - id_stheme_theme = corThemeStheme_Schema.dump( - get_id_stheme_theme) - id_stheme_theme[0]['id_site'] = d.get('id_site') + id_theme=d.get("id_theme"), id_stheme=d.get("id_stheme") + ).all() + id_stheme_theme = corThemeStheme_Schema.dump(get_id_stheme_theme) + id_stheme_theme[0]["id_site"] = d.get("id_site") site_theme_stheme = models.CorSiteSthemeTheme(**id_stheme_theme[0]) db.session.add(site_theme_stheme) db.session.commit() - return jsonify('success'), 200 + return jsonify("success"), 200 -@api.route('/api/addPhotos', methods=['POST']) +@api.route("/api/addPhotos", methods=["POST"]) @fnauth.check_auth(2) def upload_file(): - base_path = '/app/static/upload/images/' - data = request.form.getlist('data') - new_site = request.form.getlist('new_site') - uploaded_images = request.files.getlist('image') + base_path = "/app/static/upload/images/" + data = request.form.getlist("data") + new_site = request.form.getlist("new_site") + uploaded_images = request.files.getlist("image") for d in data: d_serialized = json.loads(d) check_exist = models.TPhoto.query.filter_by( - path_file_photo=d_serialized.get('path_file_photo')).first() - if(check_exist): - if (new_site == 'true'): + path_file_photo=d_serialized.get("path_file_photo") + ).first() + if check_exist: + if new_site == "true": models.TSite.query.filter_by( - id_site=d_serialized.get('id_site')).delete() + id_site=d_serialized.get("id_site") + ).delete() models.CorSiteSthemeTheme.query.filter_by( - id_site=d_serialized.get('id_site')).delete() + id_site=d_serialized.get("id_site") + ).delete() db.session.commit() - return jsonify(error='image_already_exist', image=d_serialized.get('path_file_photo')), 400 - main_photo = d_serialized.get('main_photo') - del d_serialized['main_photo'] + return ( + jsonify( + error="image_already_exist", + image=d_serialized.get("path_file_photo"), + ), + 400, + ) + main_photo = d_serialized.get("main_photo") + del d_serialized["main_photo"] photo = models.TPhoto(**d_serialized) db.session.add(photo) db.session.commit() - if (main_photo == True): + if main_photo == True: photos_query = models.TPhoto.query.filter_by( - path_file_photo=d_serialized.get('path_file_photo')).all() - photo_id = photo_schema.dump( - photos_query)[0].get('id_photo') - models.TSite.query.filter_by(id_site=d_serialized.get( - 'id_site')).update({models.TSite.main_photo: photo_id}) + path_file_photo=d_serialized.get("path_file_photo") + ).all() + photo_id = photo_schema.dump(photos_query)[0].get("id_photo") + models.TSite.query.filter_by(id_site=d_serialized.get("id_site")).update( + {models.TSite.main_photo: photo_id} + ) db.session.commit() for image in uploaded_images: image.save(os.path.join(base_path + image.filename)) - return jsonify('photo added successfully'), 200 + return jsonify("photo added successfully"), 200 -@api.route('/api/addNotices', methods=['POST']) +@api.route("/api/addNotices", methods=["POST"]) @fnauth.check_auth(2) def upload_notice(): - base_path = './static/upload/notice-photo/' - notice = request.files.get('notice') + base_path = "./static/upload/notice-photo/" + notice = request.files.get("notice") notice.save(os.path.join(base_path + notice.filename)) - return jsonify('notice added successfully'), 200 + return jsonify("notice added successfully"), 200 -@api.route('/api/deleteNotice/', methods=['DELETE']) +@api.route("/api/deleteNotice/", methods=["DELETE"]) @fnauth.check_auth(2) def delete_notice(notice): - base_path = './static/upload/notice-photo/' + base_path = "./static/upload/notice-photo/" for fileName in os.listdir(base_path): - if (fileName == notice): + if fileName == notice: os.remove(base_path + fileName) - return jsonify('notice removed successfully'), 200 + return jsonify("notice removed successfully"), 200 -@api.route('/api/updatePhoto', methods=['PATCH']) +@api.route("/api/updatePhoto", methods=["PATCH"]) @fnauth.check_auth(2) def update_photo(): - base_path = '/app/static/upload/images/' - data = request.form.get('data') - image = request.files.get('image') + base_path = "/app/static/upload/images/" + data = request.form.get("data") + image = request.files.get("image") data_serialized = json.loads(data) photos_query = models.TPhoto.query.filter_by( - id_photo=data_serialized.get('id_photo')).all() - photo_name = photo_schema.dump( - photos_query)[0].get('path_file_photo') - if (data_serialized.get('main_photo') == True): - models.TSite.query.filter_by(id_site=data_serialized.get('id_site')).update( - {models.TSite.main_photo: data_serialized.get('id_photo')}) + id_photo=data_serialized.get("id_photo") + ).all() + photo_name = photo_schema.dump(photos_query)[0].get("path_file_photo") + if data_serialized.get("main_photo") == True: + models.TSite.query.filter_by(id_site=data_serialized.get("id_site")).update( + {models.TSite.main_photo: data_serialized.get("id_photo")} + ) db.session.commit() - if (data_serialized.get('main_photo')): - del data_serialized['main_photo'] - models.TPhoto.query.filter_by( - id_photo=data_serialized.get('id_photo')).update(data_serialized) + if data_serialized.get("main_photo"): + del data_serialized["main_photo"] + models.TPhoto.query.filter_by(id_photo=data_serialized.get("id_photo")).update( + data_serialized + ) db.session.commit() - if (image): + if image: for fileName in os.listdir(base_path): if fileName.endswith(photo_name): os.remove(base_path + fileName) image.save(os.path.join(base_path + image.filename)) else: for fileName in os.listdir(base_path): - if (fileName != photo_name and fileName.endswith(photo_name)): + if fileName != photo_name and fileName.endswith(photo_name): os.remove(base_path + fileName) - return jsonify('photo added successfully'), 200 + return jsonify("photo added successfully"), 200 -@api.route('/api/deletePhotos', methods=['POST']) +@api.route("/api/deletePhotos", methods=["POST"]) @fnauth.check_auth(6) def deletePhotos(): - base_path = '/app/static/upload/images/' + base_path = "/app/static/upload/images/" photos = request.get_json() for photo in photos: photos_query = models.TPhoto.query.filter_by( - id_photo=photo.get('id_photo')).all() + id_photo=photo.get("id_photo") + ).all() photo_dump = photo_schema.dump(photos_query)[0] - photo_name = photo_dump.get('path_file_photo') - models.TPhoto.query.filter_by( - id_photo=photo.get('id_photo')).delete() - get_site_by_id = models.TSite.query.filter_by( - id_site=photo_dump.get('t_site')) + photo_name = photo_dump.get("path_file_photo") + models.TPhoto.query.filter_by(id_photo=photo.get("id_photo")).delete() + get_site_by_id = models.TSite.query.filter_by(id_site=photo_dump.get("t_site")) site = site_schema.dump(get_site_by_id)[0] - if (site.get('main_photo') == photo_dump.get('id_photo')): - models.TSite.query.filter_by(id_site=photo_dump.get( - 't_site')).update({models.TSite.main_photo: None}) + if site.get("main_photo") == photo_dump.get("id_photo"): + models.TSite.query.filter_by(id_site=photo_dump.get("t_site")).update( + {models.TSite.main_photo: None} + ) db.session.commit() for fileName in os.listdir(base_path): if fileName.endswith(photo_name): os.remove(base_path + fileName) - return jsonify('site has been deleted'), 200 + return jsonify("site has been deleted"), 200 -@api.route('/api/communes', methods=['GET']) +@api.route("/api/communes", methods=["GET"]) def returnAllcommunes(): - get_all_communes = models.Communes.query.order_by('nom_commune').all() + get_all_communes = models.Communes.query.order_by("nom_commune").all() communes = models.CommunesSchema(many=True).dump(get_all_communes) return jsonify(communes), 200 -@api.route('/api/logout', methods=['GET']) +@api.route("/api/logout", methods=["GET"]) def logout(): - resp = Response('', 200) - resp.delete_cookie('token') + resp = Response("", 200) + resp.delete_cookie("token") return resp diff --git a/backend/app.py b/backend/app.py index 743587de..caaff381 100755 --- a/backend/app.py +++ b/backend/app.py @@ -13,10 +13,11 @@ from env import db, migrate + class ReverseProxied(object): - '''Wrap the application in this middleware and configure the - front-end server to add these headers, to let you quietly bind - this to a URL other than / and to an HTTP scheme that is + """Wrap the application in this middleware and configure the + front-end server to add these headers, to let you quietly bind + this to a URL other than / and to an HTTP scheme that is different than what is used locally. In nginx: @@ -29,36 +30,38 @@ class ReverseProxied(object): } :param app: the WSGI application - ''' + """ + def __init__(self, app): self.app = app def __call__(self, environ, start_response): - script_name = environ.get('HTTP_X_SCRIPT_NAME', '') + script_name = environ.get("HTTP_X_SCRIPT_NAME", "") if script_name: - environ['SCRIPT_NAME'] = script_name - path_info = environ['PATH_INFO'] + environ["SCRIPT_NAME"] = script_name + path_info = environ["PATH_INFO"] if path_info.startswith(script_name): - environ['PATH_INFO'] = path_info[len(script_name):] + environ["PATH_INFO"] = path_info[len(script_name) :] - scheme = environ.get('HTTP_X_SCHEME', '') + scheme = environ.get("HTTP_X_SCHEME", "") if scheme: - environ['wsgi.url_scheme'] = scheme + environ["wsgi.url_scheme"] = scheme return self.app(environ, start_response) + app = Flask(__name__) -app.config['BABEL_DEFAULT_LOCALE'] = 'fr' -app.config['BABEL_TRANSLATION_DIRECTORIES'] = config.BABEL_TRANSLATION_DIRECTORIES +app.config["BABEL_DEFAULT_LOCALE"] = "fr" +app.config["BABEL_TRANSLATION_DIRECTORIES"] = config.BABEL_TRANSLATION_DIRECTORIES babel = Babel(app) -#app.wsgi_app = ReverseProxied(app.wsgi_app) +# app.wsgi_app = ReverseProxied(app.wsgi_app) CORS(app, supports_credentials=True) app.register_blueprint(main_blueprint) app.register_blueprint(api) app.register_blueprint(custom_app.custom) -app.register_blueprint(routes.routes, url_prefix='/api/auth') +app.register_blueprint(routes.routes, url_prefix="/api/auth") -app.config.from_pyfile('config.py') +app.config.from_pyfile("config.py") db.init_app(app) login_manager.init_app(app) migrate.init_app(app, db) @@ -68,9 +71,9 @@ def __call__(self, environ, start_response): def inject_to_tpl(): custom = custom_app.custom_inject_to_tpl() data = dict( - dbconf=utils.getDbConf(), - debug=app.debug, - locale=get_locale(), + dbconf=utils.getDbConf(), + debug=app.debug, + locale=get_locale(), isMultiObservatories=utils.isMultiObservatories, getThumborUrl=utils.getThumborUrl, getCustomTpl=utils.getCustomTpl, @@ -78,5 +81,6 @@ def inject_to_tpl(): data.update(custom) return data + if __name__ == "__main__": app.run(debug=True) diff --git a/backend/config.py b/backend/config.py index 2ff10f61..0963c4d0 100644 --- a/backend/config.py +++ b/backend/config.py @@ -5,18 +5,18 @@ SQLALCHEMY_MAX_OVERFLOW = 30 # Choose between 'hash' or 'md5' -PASS_METHOD = 'hash' +PASS_METHOD = "hash" TRAP_ALL_EXCEPTIONS = False COOKIE_EXPIRATION = 36000 COOKIE_AUTORENEW = True -SESSION_TYPE = 'filesystem' +SESSION_TYPE = "filesystem" SECRET_KEY = os.getenv("FLASK_SECRET_KEY") # Do not edit except in exceptional cases -BABEL_TRANSLATION_DIRECTORIES = './i18n' # From ./ dir +BABEL_TRANSLATION_DIRECTORIES = "./i18n" # From ./ dir # !!! Do not change, this is the only supported value -COMPARATOR_VERSION = 2 +COMPARATOR_VERSION = 2 # Application code for UsersHub-Authentification-Module needs -CODE_APPLICATION = os.getenv("CODE_APPLICATION") \ No newline at end of file +CODE_APPLICATION = os.getenv("CODE_APPLICATION") diff --git a/backend/env.py b/backend/env.py index 08c2f502..aee9e1f0 100644 --- a/backend/env.py +++ b/backend/env.py @@ -3,10 +3,9 @@ from flask_migrate import Migrate from flask_marshmallow import Marshmallow -os.environ['FLASK_SQLALCHEMY_DB'] = 'env.db' -os.environ['FLASK_MARSHMALLOW'] = 'env.ma' +os.environ["FLASK_SQLALCHEMY_DB"] = "env.db" +os.environ["FLASK_MARSHMALLOW"] = "env.ma" db = SQLAlchemy() migrate = Migrate() ma = Marshmallow() - diff --git a/backend/migrations/env.py b/backend/migrations/env.py index d70e23cd..70e534ac 100644 --- a/backend/migrations/env.py +++ b/backend/migrations/env.py @@ -14,26 +14,28 @@ # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') +logger = logging.getLogger("alembic.env") def include_name(name, type_, parent_names): if type_ == "schema": return name in ["geopaysages", "utilisateurs"] elif type_ == "table": - return parent_names['schema_name'] == "geopaysages" or name == "t_roles" + return parent_names["schema_name"] == "geopaysages" or name == "t_roles" elif type_ != "column": - return parent_names['table_name'] != "t_roles" + return parent_names["table_name"] != "t_roles" return ["column", "index", "unique_constraint", "foreign_key_constraint"] + + # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata config.set_main_option( - 'sqlalchemy.url', - str(current_app.extensions['migrate'].db.get_engine().url).replace( - '%', '%%')) -target_metadata = current_app.extensions['migrate'].db.metadata + "sqlalchemy.url", + str(current_app.extensions["migrate"].db.get_engine().url).replace("%", "%%"), +) +target_metadata = current_app.extensions["migrate"].db.metadata # other values from the config, defined by the needs of env.py, # can be acquired: @@ -55,12 +57,12 @@ def run_migrations_offline(): """ url = config.get_main_option("sqlalchemy.url") context.configure( - url = url, - target_metadata = target_metadata, - literal_binds = True, - include_schemas = True, - include_name = include_name, - version_table_schema = 'geopaysages' + url=url, + target_metadata=target_metadata, + literal_binds=True, + include_schemas=True, + include_name=include_name, + version_table_schema="geopaysages", ) with context.begin_transaction(): @@ -79,13 +81,13 @@ def run_migrations_online(): # when there are no changes to the schema # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): + if getattr(config.cmd_opts, "autogenerate", False): script = directives[0] if script.upgrade_ops.is_empty(): directives[:] = [] - logger.info('No changes in schema detected.') + logger.info("No changes in schema detected.") - connectable = current_app.extensions['migrate'].db.get_engine() + connectable = current_app.extensions["migrate"].db.get_engine() current_tenant = context.get_x_argument(as_dictionary=True).get("tenant") with connectable.connect() as connection: @@ -93,13 +95,13 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - include_schemas = True, - include_name = include_name, - version_table_schema = 'geopaysages', - **current_app.extensions['migrate'].configure_args + include_schemas=True, + include_name=include_name, + version_table_schema="geopaysages", + **current_app.extensions["migrate"].configure_args ) - connection.execute('ALTER ROLE ALL SET search_path = public') + connection.execute("ALTER ROLE ALL SET search_path = public") connection.dialect.default_schema_name = current_tenant with context.begin_transaction(): context.run_migrations() diff --git a/backend/migrations/versions/33990994eeae_observatory_images.py b/backend/migrations/versions/33990994eeae_observatory_images.py index ae9cbf36..434d6eea 100644 --- a/backend/migrations/versions/33990994eeae_observatory_images.py +++ b/backend/migrations/versions/33990994eeae_observatory_images.py @@ -5,30 +5,53 @@ Create Date: 2022-06-28 14:03:56.705384 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '33990994eeae' -down_revision = 'ba8ff1826771' +revision = "33990994eeae" +down_revision = "ba8ff1826771" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_observatory', sa.Column('photo', sa.String(), nullable=True), schema='geopaysages') - op.add_column('t_observatory', sa.Column('logo', sa.String(), nullable=True), schema='geopaysages') + op.add_column( + "t_observatory", + sa.Column("photo", sa.String(), nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_observatory", + sa.Column("logo", sa.String(), nullable=True), + schema="geopaysages", + ) # op.create_index('idx_t_observatory_geom', 't_observatory', ['geom'], unique=False, schema='geopaysages', postgresql_using='gist', postgresql_ops={}) - op.create_index('idx_t_site_geom', 't_site', ['geom'], unique=False, schema='geopaysages', postgresql_using='gist', postgresql_ops={}) + op.create_index( + "idx_t_site_geom", + "t_site", + ["geom"], + unique=False, + schema="geopaysages", + postgresql_using="gist", + postgresql_ops={}, + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('idx_t_site_geom', table_name='t_site', schema='geopaysages', postgresql_using='gist', postgresql_ops={}) + op.drop_index( + "idx_t_site_geom", + table_name="t_site", + schema="geopaysages", + postgresql_using="gist", + postgresql_ops={}, + ) # op.drop_index('idx_t_observatory_geom', table_name='t_observatory', schema='geopaysages', postgresql_using='gist', postgresql_ops={}) - op.drop_column('t_observatory', 'logo', schema='geopaysages') - op.drop_column('t_observatory', 'photo', schema='geopaysages') + op.drop_column("t_observatory", "logo", schema="geopaysages") + op.drop_column("t_observatory", "photo", schema="geopaysages") # ### end Alembic commands ### diff --git a/backend/migrations/versions/4a02bd35bb30_observatory_photo_to_thumbnail.py b/backend/migrations/versions/4a02bd35bb30_observatory_photo_to_thumbnail.py index dfc2b76b..79a1cf65 100644 --- a/backend/migrations/versions/4a02bd35bb30_observatory_photo_to_thumbnail.py +++ b/backend/migrations/versions/4a02bd35bb30_observatory_photo_to_thumbnail.py @@ -5,28 +5,37 @@ Create Date: 2022-09-27 13:03:47.075429 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '4a02bd35bb30' -down_revision = 'f545b345135c' +revision = "4a02bd35bb30" +down_revision = "f545b345135c" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_observatory', sa.Column('thumbnail', sa.String(), nullable=True), schema='geopaysages') + op.add_column( + "t_observatory", + sa.Column("thumbnail", sa.String(), nullable=True), + schema="geopaysages", + ) op.execute("UPDATE geopaysages.t_observatory set thumbnail = photo") - op.drop_column('t_observatory', 'photo', schema='geopaysages') + op.drop_column("t_observatory", "photo", schema="geopaysages") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_observatory', sa.Column('photo', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column( + "t_observatory", + sa.Column("photo", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) op.execute("UPDATE geopaysages.t_observatory set photo = thumbnail") - op.drop_column('t_observatory', 'thumbnail', schema='geopaysages') + op.drop_column("t_observatory", "thumbnail", schema="geopaysages") # ### end Alembic commands ### diff --git a/backend/migrations/versions/6443d436740a_site_main_observatory.py b/backend/migrations/versions/6443d436740a_site_main_observatory.py index 8a749022..53213566 100644 --- a/backend/migrations/versions/6443d436740a_site_main_observatory.py +++ b/backend/migrations/versions/6443d436740a_site_main_observatory.py @@ -5,26 +5,39 @@ Create Date: 2022-07-07 15:04:40.049161 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '6443d436740a' -down_revision = 'd7b6052f4dad' +revision = "6443d436740a" +down_revision = "d7b6052f4dad" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_site', sa.Column('main_theme_id', sa.Integer(), nullable=True), schema='geopaysages') - op.create_foreign_key(None, 't_site', 'dico_theme', ['main_theme_id'], ['id_theme'], source_schema='geopaysages', referent_schema='geopaysages') + op.add_column( + "t_site", + sa.Column("main_theme_id", sa.Integer(), nullable=True), + schema="geopaysages", + ) + op.create_foreign_key( + None, + "t_site", + "dico_theme", + ["main_theme_id"], + ["id_theme"], + source_schema="geopaysages", + referent_schema="geopaysages", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 't_site', schema='geopaysages', type_='foreignkey') - op.drop_column('t_site', 'main_theme_id', schema='geopaysages') + op.drop_constraint(None, "t_site", schema="geopaysages", type_="foreignkey") + op.drop_column("t_site", "main_theme_id", schema="geopaysages") # ### end Alembic commands ### diff --git a/backend/migrations/versions/ba8ff1826771_some_icons.py b/backend/migrations/versions/ba8ff1826771_some_icons.py index 1ffaa031..e2f39de5 100644 --- a/backend/migrations/versions/ba8ff1826771_some_icons.py +++ b/backend/migrations/versions/ba8ff1826771_some_icons.py @@ -5,26 +5,35 @@ Create Date: 2022-06-27 08:45:05.556930 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'ba8ff1826771' -down_revision = 'd8d449eefa0f' +revision = "ba8ff1826771" +down_revision = "d8d449eefa0f" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('dico_theme', sa.Column('icon', sa.String(), nullable=True), schema='geopaysages') - op.add_column('t_observatory', sa.Column('icon', sa.String(), nullable=True), schema='geopaysages') + op.add_column( + "dico_theme", + sa.Column("icon", sa.String(), nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_observatory", + sa.Column("icon", sa.String(), nullable=True), + schema="geopaysages", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('t_observatory', 'icon', schema='geopaysages') - op.drop_column('dico_theme', 'icon', schema='geopaysages') + op.drop_column("t_observatory", "icon", schema="geopaysages") + op.drop_column("dico_theme", "icon", schema="geopaysages") # ### end Alembic commands ### diff --git a/backend/migrations/versions/d7b6052f4dad_photo_id_observatory.py b/backend/migrations/versions/d7b6052f4dad_photo_id_observatory.py index 52d635f5..4a363ce3 100644 --- a/backend/migrations/versions/d7b6052f4dad_photo_id_observatory.py +++ b/backend/migrations/versions/d7b6052f4dad_photo_id_observatory.py @@ -5,26 +5,39 @@ Create Date: 2022-07-07 14:41:15.285056 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'd7b6052f4dad' -down_revision = '33990994eeae' +revision = "d7b6052f4dad" +down_revision = "33990994eeae" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_photo', sa.Column('id_observatory', sa.Integer(), nullable=True), schema='geopaysages') - op.create_foreign_key(None, 't_photo', 't_observatory', ['id_observatory'], ['id'], source_schema='geopaysages', referent_schema='geopaysages') + op.add_column( + "t_photo", + sa.Column("id_observatory", sa.Integer(), nullable=True), + schema="geopaysages", + ) + op.create_foreign_key( + None, + "t_photo", + "t_observatory", + ["id_observatory"], + ["id"], + source_schema="geopaysages", + referent_schema="geopaysages", + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, 't_photo', schema='geopaysages', type_='foreignkey') - op.drop_column('t_photo', 'id_observatory', schema='geopaysages') + op.drop_constraint(None, "t_photo", schema="geopaysages", type_="foreignkey") + op.drop_column("t_photo", "id_observatory", schema="geopaysages") # ### end Alembic commands ### diff --git a/backend/migrations/versions/d8d449eefa0f_observatory.py b/backend/migrations/versions/d8d449eefa0f_observatory.py index c1e61881..6f07c8cc 100644 --- a/backend/migrations/versions/d8d449eefa0f_observatory.py +++ b/backend/migrations/versions/d8d449eefa0f_observatory.py @@ -5,33 +5,53 @@ Create Date: 2022-06-02 08:11:40.570916 """ + from alembic import op import sqlalchemy as sa from geoalchemy2.types import Geometry # revision identifiers, used by Alembic. -revision = 'd8d449eefa0f' -down_revision = 'ffd0e83f3c4c' +revision = "d8d449eefa0f" +down_revision = "ffd0e83f3c4c" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('t_observatory', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(), nullable=True), - sa.Column('ref', sa.String(), nullable=True), - sa.Column('color', sa.String(), nullable=True), - sa.Column('comparator', sa.Enum('sidebyside', 'split', name='comparator_enum'), nullable=True), - sa.Column('geom', Geometry(geometry_type='MULTIPOLYGON', srid=4326), nullable=True), - sa.Column('is_published', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id'), - schema='geopaysages' + op.create_table( + "t_observatory", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("ref", sa.String(), nullable=True), + sa.Column("color", sa.String(), nullable=True), + sa.Column( + "comparator", + sa.Enum("sidebyside", "split", name="comparator_enum"), + nullable=True, + ), + sa.Column( + "geom", Geometry(geometry_type="MULTIPOLYGON", srid=4326), nullable=True + ), + sa.Column("is_published", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + schema="geopaysages", + ) + op.add_column( + "t_site", + sa.Column("id_observatory", sa.Integer(), nullable=True), + schema="geopaysages", + ) + op.create_foreign_key( + "t_site_fk_observatory", + "t_site", + "t_observatory", + ["id_observatory"], + ["id"], + source_schema="geopaysages", + referent_schema="geopaysages", ) - op.add_column('t_site', sa.Column('id_observatory', sa.Integer(), nullable=True), schema='geopaysages') - op.create_foreign_key('t_site_fk_observatory', 't_site', 't_observatory', ['id_observatory'], ['id'], source_schema='geopaysages', referent_schema='geopaysages') # ### end Alembic commands ### coords = """ 6.58681072 45.18200501, @@ -47,21 +67,30 @@ def upgrade(): 6.58681072 45.18200501 """ t = { - "title": "Parc national de la Vanoise", + "title": "Parc national de la Vanoise", "ref": "PNV", "color": "#b50000", "comparator": "split", - "geom": "MULTIPOLYGON ((("+ coords +")))" + "geom": "MULTIPOLYGON (((" + coords + ")))", } - op.get_bind().execute(sa.sql.text("INSERT INTO geopaysages.t_observatory (title, ref, color, comparator, geom, is_published) "+ - "values (:title, :ref, :color, :comparator, :geom, true)"), **t) - op.execute("UPDATE geopaysages.t_site set id_observatory = (SELECT id FROM geopaysages.t_observatory limit 1)") + op.get_bind().execute( + sa.sql.text( + "INSERT INTO geopaysages.t_observatory (title, ref, color, comparator, geom, is_published) " + + "values (:title, :ref, :color, :comparator, :geom, true)" + ), + **t + ) + op.execute( + "UPDATE geopaysages.t_site set id_observatory = (SELECT id FROM geopaysages.t_observatory limit 1)" + ) def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('t_site_fk_observatory', 't_site', schema='geopaysages', type_='foreignkey') - op.drop_column('t_site', 'id_observatory', schema='geopaysages') - op.drop_table('t_observatory', schema='geopaysages') + op.drop_constraint( + "t_site_fk_observatory", "t_site", schema="geopaysages", type_="foreignkey" + ) + op.drop_column("t_site", "id_observatory", schema="geopaysages") + op.drop_table("t_observatory", schema="geopaysages") # ### end Alembic commands ### op.execute("DROP TYPE comparator_enum") diff --git a/backend/migrations/versions/f545b345135c_.py b/backend/migrations/versions/f545b345135c_.py index 65aa6f13..6b29e8dd 100644 --- a/backend/migrations/versions/f545b345135c_.py +++ b/backend/migrations/versions/f545b345135c_.py @@ -5,24 +5,29 @@ Create Date: 2022-07-11 08:07:37.574533 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'f545b345135c' -down_revision = '6443d436740a' +revision = "f545b345135c" +down_revision = "6443d436740a" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('t_observatory', 'icon', schema='geopaysages') + op.drop_column("t_observatory", "icon", schema="geopaysages") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_observatory', sa.Column('icon', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column( + "t_observatory", + sa.Column("icon", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) # ### end Alembic commands ### diff --git a/backend/migrations/versions/ffd0e83f3c4c_initial_migration.py b/backend/migrations/versions/ffd0e83f3c4c_initial_migration.py index 37fb007c..cd086c32 100644 --- a/backend/migrations/versions/ffd0e83f3c4c_initial_migration.py +++ b/backend/migrations/versions/ffd0e83f3c4c_initial_migration.py @@ -5,12 +5,13 @@ Create Date: 2022-05-31 19:48:45.442620 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'ffd0e83f3c4c' +revision = "ffd0e83f3c4c" down_revision = None branch_labels = None depends_on = None @@ -19,7 +20,9 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### # ### end Alembic commands ### - op.execute('ALTER TABLE "geopaysages"."conf" ADD CONSTRAINT pk_conf PRIMARY KEY ("key")') + op.execute( + 'ALTER TABLE "geopaysages"."conf" ADD CONSTRAINT pk_conf PRIMARY KEY ("key")' + ) def downgrade(): diff --git a/backend/models.py b/backend/models.py index a1271507..820c0eea 100644 --- a/backend/models.py +++ b/backend/models.py @@ -12,43 +12,44 @@ class Conf(db.Model): - __tablename__ = 'conf' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "conf" + __table_args__ = {"schema": "geopaysages"} key = db.Column(db.String, primary_key=True) value = db.Column(db.String) + class ComparatorEnum(Enum): - sidebyside = 'sidebyside' - split = 'split' + sidebyside = "sidebyside" + split = "split" + class Observatory(db.Model): - __tablename__ = 't_observatory' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "t_observatory" + __table_args__ = {"schema": "geopaysages"} - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) title = db.Column(db.String) ref = db.Column(db.String) color = db.Column(db.String) thumbnail = db.Column(db.String) logo = db.Column(db.String) comparator = db.Column(db.Enum(ComparatorEnum, name="comparator_enum")) - geom = db.Column(Geometry(geometry_type='MULTIPOLYGON', srid=4326)) + geom = db.Column(Geometry(geometry_type="MULTIPOLYGON", srid=4326)) is_published = db.Column(db.Boolean) - class TSite(db.Model): - __tablename__ = 't_site' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "t_site" + __table_args__ = {"schema": "geopaysages"} - id_site = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) - id_observatory = db.Column(db.ForeignKey( - 'geopaysages.t_observatory.id', name='t_site_fk_observatory')) + id_site = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_observatory = db.Column( + db.ForeignKey("geopaysages.t_observatory.id", name="t_site_fk_observatory") + ) observatory = db.relationship( - 'Observatory', primaryjoin='TSite.id_observatory == Observatory.id') + "Observatory", primaryjoin="TSite.id_observatory == Observatory.id" + ) name_site = db.Column(db.String) ref_site = db.Column(db.String) desc_site = db.Column(db.String) @@ -58,131 +59,180 @@ class TSite(db.Model): alti_site = db.Column(db.Integer) path_file_guide_site = db.Column(db.String) publish_site = db.Column(db.Boolean) - geom = db.Column(Geometry(geometry_type='POINT', srid=4326)) + geom = db.Column(Geometry(geometry_type="POINT", srid=4326)) main_photo = db.Column(db.Integer) - main_theme_id = db.Column(db.ForeignKey('geopaysages.dico_theme.id_theme')) + main_theme_id = db.Column(db.ForeignKey("geopaysages.dico_theme.id_theme")) main_theme = db.relationship( - 'DicoTheme', primaryjoin='TSite.main_theme_id == DicoTheme.id_theme') + "DicoTheme", primaryjoin="TSite.main_theme_id == DicoTheme.id_theme" + ) class CorSiteSthemeTheme(db.Model): - __tablename__ = 'cor_site_stheme_theme' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "cor_site_stheme_theme" + __table_args__ = {"schema": "geopaysages"} id_site_stheme_theme = db.Column( - db.Integer, nullable=False, server_default=db.FetchedValue()) - id_site = db.Column(db.ForeignKey( - 'geopaysages.t_site.id_site'), primary_key=True, nullable=False) - id_stheme_theme = db.Column(db.ForeignKey( - 'geopaysages.cor_stheme_theme.id_stheme_theme'), primary_key=True, nullable=False) + db.Integer, nullable=False, server_default=db.FetchedValue() + ) + id_site = db.Column( + db.ForeignKey("geopaysages.t_site.id_site"), primary_key=True, nullable=False + ) + id_stheme_theme = db.Column( + db.ForeignKey("geopaysages.cor_stheme_theme.id_stheme_theme"), + primary_key=True, + nullable=False, + ) t_site = db.relationship( - 'TSite', primaryjoin='CorSiteSthemeTheme.id_site == TSite.id_site', backref='cor_site_stheme_themes') + "TSite", + primaryjoin="CorSiteSthemeTheme.id_site == TSite.id_site", + backref="cor_site_stheme_themes", + ) cor_stheme_theme = db.relationship( - 'CorSthemeTheme', primaryjoin='CorSiteSthemeTheme.id_stheme_theme == CorSthemeTheme.id_stheme_theme', backref='cor_site_stheme_themes') + "CorSthemeTheme", + primaryjoin="CorSiteSthemeTheme.id_stheme_theme == CorSthemeTheme.id_stheme_theme", + backref="cor_site_stheme_themes", + ) class CorSthemeTheme(db.Model): - __tablename__ = 'cor_stheme_theme' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "cor_stheme_theme" + __table_args__ = {"schema": "geopaysages"} id_stheme_theme = db.Column( - db.Integer, nullable=False, unique=True, server_default=db.FetchedValue()) - id_stheme = db.Column(db.ForeignKey( - 'geopaysages.dico_stheme.id_stheme'), primary_key=True, nullable=False) - id_theme = db.Column(db.ForeignKey( - 'geopaysages.dico_theme.id_theme'), primary_key=True, nullable=False) + db.Integer, nullable=False, unique=True, server_default=db.FetchedValue() + ) + id_stheme = db.Column( + db.ForeignKey("geopaysages.dico_stheme.id_stheme"), + primary_key=True, + nullable=False, + ) + id_theme = db.Column( + db.ForeignKey("geopaysages.dico_theme.id_theme"), + primary_key=True, + nullable=False, + ) dico_stheme = db.relationship( - 'DicoStheme', primaryjoin='CorSthemeTheme.id_stheme == DicoStheme.id_stheme', backref='cor_stheme_themes') + "DicoStheme", + primaryjoin="CorSthemeTheme.id_stheme == DicoStheme.id_stheme", + backref="cor_stheme_themes", + ) dico_theme = db.relationship( - 'DicoTheme', primaryjoin='CorSthemeTheme.id_theme == DicoTheme.id_theme', backref='cor_stheme_themes') + "DicoTheme", + primaryjoin="CorSthemeTheme.id_theme == DicoTheme.id_theme", + backref="cor_stheme_themes", + ) class DicoLicencePhoto(db.Model): - __tablename__ = 'dico_licence_photo' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "dico_licence_photo" + __table_args__ = {"schema": "geopaysages"} id_licence_photo = db.Column( - db.Integer, primary_key=True, server_default=db.FetchedValue()) + db.Integer, primary_key=True, server_default=db.FetchedValue() + ) name_licence_photo = db.Column(db.String) description_licence_photo = db.Column(db.String) class DicoStheme(db.Model): - __tablename__ = 'dico_stheme' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "dico_stheme" + __table_args__ = {"schema": "geopaysages"} - id_stheme = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id_stheme = db.Column( + db.Integer, primary_key=True, server_default=db.FetchedValue() + ) name_stheme = db.Column(db.String) class DicoTheme(db.Model): - __tablename__ = 'dico_theme' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "dico_theme" + __table_args__ = {"schema": "geopaysages"} - id_theme = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id_theme = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) name_theme = db.Column(db.String) icon = db.Column(db.String) class TRole(db.Model): - __tablename__ = 't_roles' - __table_args__ = {'schema': 'utilisateurs', 'extend_existing': True} + __tablename__ = "t_roles" + __table_args__ = {"schema": "utilisateurs", "extend_existing": True} - groupe = db.Column(db.Boolean, nullable=False, - server_default=db.FetchedValue()) - id_role = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + groupe = db.Column(db.Boolean, nullable=False, server_default=db.FetchedValue()) + id_role = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) identifiant = db.Column(db.String(100)) nom_role = db.Column(db.String(50)) prenom_role = db.Column(db.String(50)) desc_role = db.Column(db.Text) - _pass = db.Column('pass', db.String(100)) - _pass_plus = db.Column('pass_plus', db.String(100)) + _pass = db.Column("pass", db.String(100)) + _pass_plus = db.Column("pass_plus", db.String(100)) email = db.Column(db.String(250)) - id_organisme = db.Column('id_organisme', db.INTEGER(), autoincrement=False, nullable=True) + id_organisme = db.Column( + "id_organisme", db.INTEGER(), autoincrement=False, nullable=True + ) remarques = db.Column(db.Text) date_insert = db.Column(db.DateTime) date_update = db.Column(db.DateTime) - uuid_role = db.Column('uuid_role', postgresql.UUID(), server_default=db.text('uuid_generate_v4()'), autoincrement=False, nullable=False) - active = db.Column('active', db.BOOLEAN(), server_default=db.text('true'), autoincrement=False, nullable=True) - champs_addi = db.Column('champs_addi', postgresql.JSONB(astext_type=db.Text()), autoincrement=False, nullable=True) + uuid_role = db.Column( + "uuid_role", + postgresql.UUID(), + server_default=db.text("uuid_generate_v4()"), + autoincrement=False, + nullable=False, + ) + active = db.Column( + "active", + db.BOOLEAN(), + server_default=db.text("true"), + autoincrement=False, + nullable=True, + ) + champs_addi = db.Column( + "champs_addi", + postgresql.JSONB(astext_type=db.Text()), + autoincrement=False, + nullable=True, + ) class TPhoto(db.Model): - __tablename__ = 't_photo' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "t_photo" + __table_args__ = {"schema": "geopaysages"} - id_photo = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) - id_site = db.Column(db.ForeignKey('geopaysages.t_site.id_site')) - id_observatory = db.Column(db.ForeignKey('geopaysages.t_observatory.id')) + id_photo = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + id_site = db.Column(db.ForeignKey("geopaysages.t_site.id_site")) + id_observatory = db.Column(db.ForeignKey("geopaysages.t_observatory.id")) path_file_photo = db.Column(db.String) - id_role = db.Column(db.ForeignKey('utilisateurs.t_roles.id_role')) + id_role = db.Column(db.ForeignKey("utilisateurs.t_roles.id_role")) date_photo = db.Column(db.String) filter_date = db.Column(db.Date) legende_photo = db.Column(db.String) display_gal_photo = db.Column(db.Boolean) - id_licence_photo = db.Column(db.ForeignKey( - 'geopaysages.dico_licence_photo.id_licence_photo')) + id_licence_photo = db.Column( + db.ForeignKey("geopaysages.dico_licence_photo.id_licence_photo") + ) dico_licence_photo = db.relationship( - 'DicoLicencePhoto', primaryjoin='TPhoto.id_licence_photo == DicoLicencePhoto.id_licence_photo', backref='t_photos') + "DicoLicencePhoto", + primaryjoin="TPhoto.id_licence_photo == DicoLicencePhoto.id_licence_photo", + backref="t_photos", + ) t_role = db.relationship( - 'TRole', primaryjoin='TPhoto.id_role == TRole.id_role', backref='t_photos') + "TRole", primaryjoin="TPhoto.id_role == TRole.id_role", backref="t_photos" + ) t_site = db.relationship( - 'TSite', primaryjoin='TPhoto.id_site == TSite.id_site', backref='t_photos') + "TSite", primaryjoin="TPhoto.id_site == TSite.id_site", backref="t_photos" + ) class Communes(db.Model): - __tablename__ = 'communes' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "communes" + __table_args__ = {"schema": "geopaysages"} - code_commune = db.Column(db.String, primary_key=True, - server_default=db.FetchedValue()) + code_commune = db.Column( + db.String, primary_key=True, server_default=db.FetchedValue() + ) nom_commune = db.Column(db.String) @@ -191,8 +241,11 @@ def _serialize(self, value, attr, obj): if value is None: return value else: - if attr == 'geom': - return [db.session.scalar(geo_funcs.ST_Y(value)), db.session.scalar(geo_funcs.ST_X(value))] + if attr == "geom": + return [ + db.session.scalar(geo_funcs.ST_Y(value)), + db.session.scalar(geo_funcs.ST_X(value)), + ] else: return None @@ -200,17 +253,22 @@ def _deserialize(self, value, attr, data): if value is None: return value else: - if attr == 'geom': - return WKTGeographyElement('POINT({0} {1})'.format(str(value.get('longitude')), str(value.get('latitude')))) + if attr == "geom": + return WKTGeographyElement( + "POINT({0} {1})".format( + str(value.get("longitude")), str(value.get("latitude")) + ) + ) else: return None -#schemas# + +# schemas# class DicoThemeSchema(ma.SQLAlchemyAutoSchema): class Meta: - fields = ('id_theme', 'name_theme', 'icon') + fields = ("id_theme", "name_theme", "icon") class DicoSthemeSchema(ma.SQLAlchemyAutoSchema): @@ -221,19 +279,17 @@ class Meta: class CorThemeSthemeSchema(ma.SQLAlchemyAutoSchema): class Meta: - fields = ('id_stheme_theme',) + fields = ("id_stheme_theme",) class LicencePhotoSchema(ma.SQLAlchemyAutoSchema): class Meta: - fields = ('id_licence_photo', 'name_licence_photo', - 'description_licence_photo') + fields = ("id_licence_photo", "name_licence_photo", "description_licence_photo") class RoleSchema(ma.SQLAlchemyAutoSchema): class Meta: - fields = ('id_role', 'identifiant', 'nom_role', - 'id_organisme') + fields = ("id_role", "identifiant", "nom_role", "id_organisme") class TPhotoSchema(ma.SQLAlchemyAutoSchema): @@ -247,34 +303,34 @@ class Meta: class CorSthemeThemeSchema(ma.SQLAlchemyAutoSchema): dico_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "name_theme"]) - dico_stheme = ma.Nested(DicoSthemeSchema, only=[ - "id_stheme", "name_stheme"]) + dico_stheme = ma.Nested(DicoSthemeSchema, only=["id_stheme", "name_stheme"]) class Meta: - fields = ('dico_theme', 'dico_stheme') - #model = CorSthemeTheme + fields = ("dico_theme", "dico_stheme") + # model = CorSthemeTheme class ObservatorySchema(ma.SQLAlchemyAutoSchema): comparator = EnumField(ComparatorEnum, by_value=True) geom = fields.Method("geomSerialize") - + @staticmethod def geomSerialize(obj): if obj.geom is None: return None p = to_shape(obj.geom) - s = p.simplify(.0001, preserve_topology=False) + s = p.simplify(0.0001, preserve_topology=False) return s.wkt class Meta: model = Observatory include_relationships = True + class ObservatorySchemaFull(ma.SQLAlchemyAutoSchema): comparator = EnumField(ComparatorEnum, by_value=True) geom = fields.Method("geomSerialize") - + @staticmethod def geomSerialize(obj): if obj.geom is None: @@ -286,16 +342,17 @@ class Meta: model = Observatory include_relationships = True + class ObservatorySchemaLite(ma.SQLAlchemyAutoSchema): comparator = EnumField(ComparatorEnum, by_value=False) geom = fields.Method("geomSerialize") - + @staticmethod def geomSerialize(obj): if obj.geom is None: return None p = to_shape(obj.geom) - s = p.simplify(.001, preserve_topology=True) + s = p.simplify(0.001, preserve_topology=True) return s.wkt class Meta: @@ -304,8 +361,10 @@ class Meta: class TSiteSchema(ma.SQLAlchemyAutoSchema): - geom = GeographySerializationField(attribute='geom') - observatory = ma.Nested(ObservatorySchema, only=["id", "title", "ref", "color", "logo"]) + geom = GeographySerializationField(attribute="geom") + observatory = ma.Nested( + ObservatorySchema, only=["id", "title", "ref", "color", "logo"] + ) main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "name_theme", "icon"]) class Meta: diff --git a/backend/routes.py b/backend/routes.py index bf8661ba..19b7b6db 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -9,9 +9,9 @@ import math import os -main = Blueprint('main', __name__, template_folder='tpl') +main = Blueprint("main", __name__, template_folder="tpl") -from env import db +from env import db dicotheme_schema = models.DicoThemeSchema(many=True) dicostheme_schema = models.DicoSthemeSchema(many=True) @@ -22,10 +22,11 @@ communes_schema = models.CommunesSchema(many=True) - -@main.route('/') +@main.route("/") def home(): - sql = text("SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6") + sql = text( + "SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6" + ) sites_proxy = db.engine.execute(sql).fetchall() sites = [dict(row.items()) for row in sites_proxy] @@ -38,25 +39,27 @@ def home(): sites_without_photo = [] code_communes = [] for site in sites: - photo_id = site.get('main_photo') + photo_id = site.get("main_photo") if photo_id: - photo_ids.append(site.get('main_photo')) + photo_ids.append(site.get("main_photo")) else: - sites_without_photo.append(str(site.get('id_site'))) - code_communes.append(site.get('code_city_site')) + sites_without_photo.append(str(site.get("id_site"))) + code_communes.append(site.get("code_city_site")) - query_photos = models.TPhoto.query.filter( - models.TPhoto.id_photo.in_(photo_ids) - ) + query_photos = models.TPhoto.query.filter(models.TPhoto.id_photo.in_(photo_ids)) dump_photos = photo_schema.dump(query_photos) # WAHO tordu l'histoire! if len(sites_without_photo): - sql_missing_photos_str = "select distinct on (id_site) p.* from geopaysages.t_photo p join geopaysages.t_observatory o on o.id=p.id_observatory where p.id_site IN (" + ",".join(sites_without_photo) + ") and o.is_published is true order by id_site, filter_date desc" + sql_missing_photos_str = ( + "select distinct on (id_site) p.* from geopaysages.t_photo p join geopaysages.t_observatory o on o.id=p.id_observatory where p.id_site IN (" + + ",".join(sites_without_photo) + + ") and o.is_published is true order by id_site, filter_date desc" + ) sql_missing_photos = text(sql_missing_photos_str) missing_photos_result = db.engine.execute(sql_missing_photos).fetchall() missing_photos = [dict(row) for row in missing_photos_result] for missing_photo in missing_photos: - missing_photo['t_site'] = missing_photo.get('id_site') + missing_photo["t_site"] = missing_photo.get("id_site") dump_photos.append(missing_photo) query_commune = models.Communes.query.filter( @@ -64,131 +67,177 @@ def home(): ) dump_communes = communes_schema.dump(query_commune) for site in sites: - id_site = site.get('id_site') + id_site = site.get("id_site") photo = None try: - photo = next(photo for photo in dump_photos if (photo.get('t_site') == id_site)) + photo = next( + photo for photo in dump_photos if (photo.get("t_site") == id_site) + ) except StopIteration: pass if photo: - site['photo'] = photo.get('path_file_photo') #utils.getMedium(photo).get('output_url') - site['commune'] = next(commune for commune in dump_communes if (commune.get('code_commune') == site.get('code_city_site'))) - - all_sites=site_schema.dump(models.TSite.query.join(models.Observatory).filter(models.TSite.publish_site == True, models.Observatory.is_published == True)) + site["photo"] = photo.get( + "path_file_photo" + ) # utils.getMedium(photo).get('output_url') + site["commune"] = next( + commune + for commune in dump_communes + if (commune.get("code_commune") == site.get("code_city_site")) + ) + + all_sites = site_schema.dump( + models.TSite.query.join(models.Observatory).filter( + models.TSite.publish_site == True, models.Observatory.is_published == True + ) + ) - carousel_photos = [fileName for fileName in os.listdir('/app/static/custom/home-carousel')] - carousel_photos = list(filter(lambda x: x != '.gitkeep', carousel_photos)) + carousel_photos = [ + fileName for fileName in os.listdir("/app/static/custom/home-carousel") + ] + carousel_photos = list(filter(lambda x: x != ".gitkeep", carousel_photos)) - if (utils.isMultiObservatories() == True ) : - observatories = models.Observatory.query.filter(models.Observatory.is_published == True).order_by(models.Observatory.title) + if utils.isMultiObservatories() == True: + observatories = models.Observatory.query.filter( + models.Observatory.is_published == True + ).order_by(models.Observatory.title) dump_observatories = observatory_schema_lite.dump(observatories) col_max = 5 - nb_obs = len(dump_observatories)+1 - nb_rows = math.ceil( nb_obs / col_max ) - nb_cols = math.ceil( nb_obs / nb_rows ) - - - patchwork_options = { - "nb_cols" : nb_cols - } - return render_template('home_multi_obs.jinja', carousel_photos=carousel_photos, observatories=dump_observatories, sites=all_sites, patchwork_options=patchwork_options) + nb_obs = len(dump_observatories) + 1 + nb_rows = math.ceil(nb_obs / col_max) + nb_cols = math.ceil(nb_obs / nb_rows) + + patchwork_options = {"nb_cols": nb_cols} + return render_template( + "home_multi_obs.jinja", + carousel_photos=carousel_photos, + observatories=dump_observatories, + sites=all_sites, + patchwork_options=patchwork_options, + ) + + return render_template( + "home_mono_obs.jinja", + carousel_photos=carousel_photos, + blocks=sites, + sites=all_sites, + ) - return render_template('home_mono_obs.jinja', carousel_photos=carousel_photos, blocks=sites, sites=all_sites) -@main.route('/gallery') +@main.route("/gallery") def gallery(): data = utils.getFiltersData() - return render_template('gallery.jinja', filters=data['filters'], sites=data['sites'], observatories=data['observatories']) + return render_template( + "gallery.jinja", + filters=data["filters"], + sites=data["sites"], + observatories=data["observatories"], + ) -@main.route('/sites/') +@main.route("/sites/") def site(id_site): - get_site_by_id = models.TSite.query.filter_by(id_site = id_site, publish_site = True) - site=site_schema.dump(get_site_by_id) + get_site_by_id = models.TSite.query.filter_by(id_site=id_site, publish_site=True) + site = site_schema.dump(get_site_by_id) if len(site) == 0: return abort(404) site = site[0] - - get_villes = models.Communes.query.filter_by(code_commune = site.get('code_city_site')) - site['ville'] = communes_schema.dump(get_villes)[0] - get_photos_by_site = models.TPhoto.query.filter_by(id_site = id_site, display_gal_photo=True).order_by('filter_date') + get_villes = models.Communes.query.filter_by( + code_commune=site.get("code_city_site") + ) + site["ville"] = communes_schema.dump(get_villes)[0] + + get_photos_by_site = models.TPhoto.query.filter_by( + id_site=id_site, display_gal_photo=True + ).order_by("filter_date") photos = photo_schema.dump(get_photos_by_site) - cor_sthemes_themes = site.get('cor_site_stheme_themes') + cor_sthemes_themes = site.get("cor_site_stheme_themes") cor_list = [] subthemes_list = [] for cor in cor_sthemes_themes: - cor_list.append(cor.get('id_stheme_theme')) + cor_list.append(cor.get("id_stheme_theme")) query = models.CorSthemeTheme.query.filter( - models.CorSthemeTheme.id_stheme_theme.in_(cor_list)) + models.CorSthemeTheme.id_stheme_theme.in_(cor_list) + ) themes_sthemes = themes_sthemes_schema.dump(query) for item in themes_sthemes: - if item.get('dico_stheme').get('id_stheme') not in subthemes_list: - subthemes_list.append(item.get('dico_stheme').get('name_stheme')) - - site['stheme'] = list(set(subthemes_list)) + if item.get("dico_stheme").get("id_stheme") not in subthemes_list: + subthemes_list.append(item.get("dico_stheme").get("name_stheme")) + + site["stheme"] = list(set(subthemes_list)) def getPhoto(photo): captions = [] - licence_photo = photo.get('dico_licence_photo') + licence_photo = photo.get("dico_licence_photo") if licence_photo: - captions.append(licence_photo.get('name_licence_photo')) - caption = ' | '.join(captions) + captions.append(licence_photo.get("name_licence_photo")) + caption = " | ".join(captions) return { - 'id': photo.get('id_photo'), - 'filename': photo.get('path_file_photo'), - 'shot_on': photo.get('filter_date'), - 'date_approx': photo.get('date_photo'), - 'caption': caption + "id": photo.get("id_photo"), + "filename": photo.get("path_file_photo"), + "shot_on": photo.get("filter_date"), + "date_approx": photo.get("date_photo"), + "caption": caption, } photos = [getPhoto(photo) for photo in photos] - - return render_template('site.jinja', site=site, photos=photos, comparator_version=COMPARATOR_VERSION) + + return render_template( + "site.jinja", site=site, photos=photos, comparator_version=COMPARATOR_VERSION + ) -@main.route('/sites//photos/latest') +@main.route("/sites//photos/latest") def site_photos_last(id_site): - get_site_by_id = models.TSite.query.filter_by(id_site = id_site, publish_site = True) - site=site_schema.dump(get_site_by_id) + get_site_by_id = models.TSite.query.filter_by(id_site=id_site, publish_site=True) + site = site_schema.dump(get_site_by_id) if len(site) == 0: return abort(404) site = site[0] - get_photos_by_site = models.TPhoto.query.filter_by(id_site = id_site, display_gal_photo=True).order_by(desc(models.TPhoto.filter_date)).limit(1) + get_photos_by_site = ( + models.TPhoto.query.filter_by(id_site=id_site, display_gal_photo=True) + .order_by(desc(models.TPhoto.filter_date)) + .limit(1) + ) photos = photo_schema.dump(get_photos_by_site) - photo=photos[0] + photo = photos[0] - date_approx = photo.get('date_photo') + date_approx = photo.get("date_photo") if date_approx: - photo['date_display'] = date_approx + photo["date_display"] = date_approx else: - date_obj = datetime.strptime(photo.get('filter_date'), '%Y-%m-%d') - photo['date_display'] = date_obj.strftime('%d-%m-%Y') + date_obj = datetime.strptime(photo.get("filter_date"), "%Y-%m-%d") + photo["date_display"] = date_obj.strftime("%d-%m-%Y") - return render_template('site_photo.jinja', site=site, photo=photo) + return render_template("site_photo.jinja", site=site, photo=photo) -@main.route('/sites') +@main.route("/sites") def sites(): data = utils.getFiltersData() - return render_template('sites.jinja', filters=data['filters'], sites=data['sites'], observatories=data['observatories']) + return render_template( + "sites.jinja", + filters=data["filters"], + sites=data["sites"], + observatories=data["observatories"], + ) -@main.route('/legal-notices') +@main.route("/legal-notices") def legal_notices(): - tpl = utils.getCustomTpl('legal_notices') + tpl = utils.getCustomTpl("legal_notices") if not tpl: return abort(404) - return render_template(tpl) \ No newline at end of file + return render_template(tpl) diff --git a/backend/utils.py b/backend/utils.py index d4c326ef..0890cb8f 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -21,31 +21,40 @@ site_schema = models.TSiteSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) + def getCustomTpl(name): - tpl_local = f'custom/{name}_{get_locale().__str__()}.jinja' - tpl_common = f'custom/{name}.jinja' - if os.path.exists(f'tpl/{tpl_local}'): + tpl_local = f"custom/{name}_{get_locale().__str__()}.jinja" + tpl_common = f"custom/{name}.jinja" + if os.path.exists(f"tpl/{tpl_local}"): return tpl_local - if os.path.exists(f'tpl/{tpl_common}'): + if os.path.exists(f"tpl/{tpl_common}"): return tpl_common return None + def getThumborSignature(url): - key = bytes(os.getenv("THUMBOR_SECURITY_KEY"), 'UTF-8') - msg = bytes(url, 'UTF-8') + key = bytes(os.getenv("THUMBOR_SECURITY_KEY"), "UTF-8") + msg = bytes(url, "UTF-8") h = hmac.new(key, msg, hashlib.sha1) return urlsafe_b64encode(h.digest()).decode("ascii") + def getThumborUrl(params, filename): - if params.startswith('/'): + if params.startswith("/"): params = params[1:] - url = params + '/' + urllib.parse.quote(f'http://backend/static/upload/images/{filename}', safe='') + url = ( + params + + "/" + + urllib.parse.quote(f"http://backend/static/upload/images/{filename}", safe="") + ) signature = getThumborSignature(url) - return f'/thumbor/{signature}/{url}' + return f"/thumbor/{signature}/{url}" + def getRandStr(nb): chars = string.ascii_lowercase + string.digits - return ''.join(random.choice(chars) for i in range(nb)) + return "".join(random.choice(chars) for i in range(nb)) + def getDbConf(): sql = text("SELECT key, value FROM geopaysages.conf") @@ -54,209 +63,240 @@ def getDbConf(): conf = {} for row in rows: try: - conf[row.get('key')] = json.loads(row.get('value')) + conf[row.get("key")] = json.loads(row.get("value")) except Exception as exception: - conf[row.get('key')] = row.get('value') + conf[row.get("key")] = row.get("value") - conf['default_sort_sites'] = conf.get('default_sort_sites', 'name_site') + conf["default_sort_sites"] = conf.get("default_sort_sites", "name_site") return conf + def isMultiObservatories(): # Pourrait passer par un count sql sql = text("SELECT id FROM geopaysages.t_observatory where is_published is true") result = db.engine.execute(sql).fetchall() rows = [dict(row) for row in result] - if len(rows) > 1 : + if len(rows) > 1: return True return False + def getFiltersData(): dbconf = getDbConf() - sites=site_schema.dump(models.TSite.query.join(models.Observatory).filter(models.TSite.publish_site == True, models.Observatory.is_published == True).order_by(dbconf['default_sort_sites'])) + sites = site_schema.dump( + models.TSite.query.join(models.Observatory) + .filter( + models.TSite.publish_site == True, models.Observatory.is_published == True + ) + .order_by(dbconf["default_sort_sites"]) + ) for site in sites: - cor_sthemes_themes = site.get('cor_site_stheme_themes') + cor_sthemes_themes = site.get("cor_site_stheme_themes") cor_list = [] themes_list = [] subthemes_list = [] for cor in cor_sthemes_themes: - cor_list.append(cor.get('id_stheme_theme')) + cor_list.append(cor.get("id_stheme_theme")) query = models.CorSthemeTheme.query.filter( - models.CorSthemeTheme.id_stheme_theme.in_(cor_list)) + models.CorSthemeTheme.id_stheme_theme.in_(cor_list) + ) themes_sthemes = themes_sthemes_schema.dump(query) for item in themes_sthemes: - if item.get('dico_theme').get('id_theme') not in themes_list: - themes_list.append(item.get('dico_theme').get('id_theme')) - if item.get('dico_stheme').get('id_stheme') not in subthemes_list: - subthemes_list.append(item.get('dico_stheme').get('id_stheme')) + if item.get("dico_theme").get("id_theme") not in themes_list: + themes_list.append(item.get("dico_theme").get("id_theme")) + if item.get("dico_stheme").get("id_stheme") not in subthemes_list: + subthemes_list.append(item.get("dico_stheme").get("id_stheme")) - get_photos_by_site = models.TPhoto.query.filter_by( - id_site=site.get('id_site')) + get_photos_by_site = models.TPhoto.query.filter_by(id_site=site.get("id_site")) photos = photo_schema.dump(get_photos_by_site) - site['link'] = url_for('main.site', id_site=site.get('id_site'), _external=True) - site['latlon'] = site.get('geom') - site['themes'] = themes_list - site['subthemes'] = subthemes_list - site['township'] = site.get('code_city_site') + site["link"] = url_for("main.site", id_site=site.get("id_site"), _external=True) + site["latlon"] = site.get("geom") + site["themes"] = themes_list + site["subthemes"] = subthemes_list + site["township"] = site.get("code_city_site") - site['years'] = set() + site["years"] = set() for photo in photos: - year = str(photo.get('filter_date')).split('-')[0] - site['years'].add(year) - photo['year'] = year - site['years'] = list(site['years']) - site['photos'] = photos + year = str(photo.get("filter_date")).split("-")[0] + site["years"].add(year) + photo["year"] = year + site["years"] = list(site["years"]) + site["photos"] = photos subthemes = dicostheme_schema.dump(models.DicoStheme.query.all()) for sub in subthemes: themes_of_subthemes = [] - for item in sub.get('cor_stheme_themes'): - themes_of_subthemes.append(item.get('id_theme')) - sub['themes'] = themes_of_subthemes - - filters = [{ - 'name': 'themes', - 'label': gettext(u'sites.filter.themes'), - 'items': set() - }, { - 'name': 'subthemes', - 'label': gettext(u'sites.filter.subthemes'), - 'items': set() - }, { - 'name': 'township', - 'hideNoMatched': True, - 'label': gettext(u'sites.filter.township'), - 'items': set() - }, { - 'name': 'years', - 'hideNoMatched': True, - 'label': gettext(u'sites.filter.years'), - 'items': set() - }] + for item in sub.get("cor_stheme_themes"): + themes_of_subthemes.append(item.get("id_theme")) + sub["themes"] = themes_of_subthemes + + filters = [ + {"name": "themes", "label": gettext("sites.filter.themes"), "items": set()}, + { + "name": "subthemes", + "label": gettext("sites.filter.subthemes"), + "items": set(), + }, + { + "name": "township", + "hideNoMatched": True, + "label": gettext("sites.filter.township"), + "items": set(), + }, + { + "name": "years", + "hideNoMatched": True, + "label": gettext("sites.filter.years"), + "items": set(), + }, + ] for site in sites: # Compute the prop years - site['years'] = set() - for photo in site.get('photos'): - site['years'].add(photo.get('year')) - site['years'] = list(site['years']) + site["years"] = set() + for photo in site.get("photos"): + site["years"].add(photo.get("year")) + site["years"] = list(site["years"]) for filter in filters: - val = site.get(filter.get('name')) + val = site.get(filter.get("name")) if isinstance(val, (list, set)): - filter.get('items').update(val) + filter.get("items").update(val) else: - filter.get('items').add(val) + filter.get("items").add(val) themes = dicotheme_schema.dump(models.DicoTheme.query.all()) - themes = [{ - 'id': item['id_theme'], - 'label': item['name_theme'], - 'icon': item['icon'], - } for item in themes] - - subthemes = [{ - 'id': item['id_stheme'], - 'label': item['name_stheme'], - 'themes': item['themes'] - } for item in subthemes] + themes = [ + { + "id": item["id_theme"], + "label": item["name_theme"], + "icon": item["icon"], + } + for item in themes + ] + + subthemes = [ + { + "id": item["id_stheme"], + "label": item["name_stheme"], + "themes": item["themes"], + } + for item in subthemes + ] filter_township = [ - filter for filter in filters if filter.get('name') == 'township'][0] - str_map_in = ["'" + township + - "'" for township in filter_township.get('items')] - sql_map_str = "SELECT code_commune AS id, nom_commune AS label FROM geopaysages.communes WHERE code_commune IN (" + ",".join( - str_map_in) + ")" + filter for filter in filters if filter.get("name") == "township" + ][0] + str_map_in = ["'" + township + "'" for township in filter_township.get("items")] + sql_map_str = ( + "SELECT code_commune AS id, nom_commune AS label FROM geopaysages.communes WHERE code_commune IN (" + + ",".join(str_map_in) + + ")" + ) sql_map = text(sql_map_str) townships_result = db.engine.execute(sql_map).fetchall() townships = [dict(row) for row in townships_result] for site in sites: - site['ville'] = next(township for township in townships if township.get('id') == site.get('township')) - - dbs = { - 'themes': themes, - 'subthemes': subthemes, - 'township': townships - } - + site["ville"] = next( + township + for township in townships + if township.get("id") == site.get("township") + ) + + dbs = {"themes": themes, "subthemes": subthemes, "township": townships} + photo_ids = [] sites_without_photo = [] for site in sites: - photo_id = site.get('main_photo') + photo_id = site.get("main_photo") if photo_id: - photo_ids.append(site.get('main_photo')) + photo_ids.append(site.get("main_photo")) else: - sites_without_photo.append(str(site.get('id_site'))) + sites_without_photo.append(str(site.get("id_site"))) - query_photos = models.TPhoto.query.filter( - models.TPhoto.id_photo.in_(photo_ids) - ) + query_photos = models.TPhoto.query.filter(models.TPhoto.id_photo.in_(photo_ids)) dump_photos = photo_schema.dump(query_photos) if len(sites_without_photo): - sql_missing_photos_str = "select distinct on (id_site) * from geopaysages.t_photo where id_site IN (" + ",".join(sites_without_photo) + ") order by id_site, filter_date desc" + sql_missing_photos_str = ( + "select distinct on (id_site) * from geopaysages.t_photo where id_site IN (" + + ",".join(sites_without_photo) + + ") order by id_site, filter_date desc" + ) sql_missing_photos = text(sql_missing_photos_str) missing_photos_result = db.engine.execute(sql_missing_photos).fetchall() missing_photos = [dict(row) for row in missing_photos_result] for missing_photo in missing_photos: - missing_photo['t_site'] = missing_photo.get('id_site') + missing_photo["t_site"] = missing_photo.get("id_site") dump_photos.append(missing_photo) for site in sites: - id_site = site.get('id_site') + id_site = site.get("id_site") try: - photo = next(photo for photo in dump_photos if (photo.get('t_site') == id_site)) - site['photo'] = photo.get('path_file_photo') #getThumbnail(photo).get('output_url') + photo = next( + photo for photo in dump_photos if (photo.get("t_site") == id_site) + ) + site["photo"] = photo.get( + "path_file_photo" + ) # getThumbnail(photo).get('output_url') except StopIteration: pass def getItem(name, id): - return next(item for item in dbs.get(name) if item.get('id') == id) + return next(item for item in dbs.get(name) if item.get("id") == id) for filter in filters: - if (filter.get('name') == 'years'): - filter['items'] = [{ - 'label': str(year), - 'id': year - } for year in filter.get('items')] - filter['items'] = sorted(filter['items'], key=lambda k: k['label'], reverse=True) + if filter.get("name") == "years": + filter["items"] = [ + {"label": str(year), "id": year} for year in filter.get("items") + ] + filter["items"] = sorted( + filter["items"], key=lambda k: k["label"], reverse=True + ) else: - filter['items'] = [getItem(filter.get('name'), item_id) - for item_id in filter.get('items')] - filter['items'] = sorted(filter['items'], key=lambda k: k['label']) + filter["items"] = [ + getItem(filter.get("name"), item_id) for item_id in filter.get("items") + ] + filter["items"] = sorted(filter["items"], key=lambda k: k["label"]) observatories = [] for site in sites: try: - next((item for item in observatories if item["id"] == site['id_observatory'])) + next( + (item for item in observatories if item["id"] == site["id_observatory"]) + ) except StopIteration: - observatory_row = models.Observatory.query.filter_by(id = site['id_observatory']) - observatory=observatory_schema.dump(observatory_row) - observatory=observatory[0] - observatories.append({ - 'id': site['id_observatory'], - 'label': site['observatory']['title'], - 'data': { - 'geom': observatory['geom'], - 'color': observatory['color'], - 'logo': observatory['logo'] + observatory_row = models.Observatory.query.filter_by( + id=site["id_observatory"] + ) + observatory = observatory_schema.dump(observatory_row) + observatory = observatory[0] + observatories.append( + { + "id": site["id_observatory"], + "label": site["observatory"]["title"], + "data": { + "geom": observatory["geom"], + "color": observatory["color"], + "logo": observatory["logo"], + }, } - }) + ) - observatories = sorted(observatories, key=lambda d: d['label']) + observatories = sorted(observatories, key=lambda d: d["label"]) if len(observatories) > 1: - filters.insert(0, { - 'name': 'id_observatory', - 'label': gettext(u'sites.filter.obervatories'), - 'items': observatories - }) - - return { - 'filters': filters, - 'sites': sites, - 'observatories': observatories - } \ No newline at end of file + filters.insert( + 0, + { + "name": "id_observatory", + "label": gettext("sites.filter.obervatories"), + "items": observatories, + }, + ) + + return {"filters": filters, "sites": sites, "observatories": observatories} From ea9e85687351fc8db78cd70a0089100929bc23d9 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 12:04:47 +0200 Subject: [PATCH 006/107] feat: wip add translation table --- backend/api.py | 12 +- .../versions/d7fd422e1054_translations.py | 223 ++++++++++++++++++ backend/models.py | 210 +++++++++++++++-- 3 files changed, 421 insertions(+), 24 deletions(-) create mode 100644 backend/migrations/versions/d7fd422e1054_translations.py diff --git a/backend/api.py b/backend/api.py index b60f7946..5c002890 100644 --- a/backend/api.py +++ b/backend/api.py @@ -79,7 +79,11 @@ def returnDdConf(): @api.route("/api/observatories", methods=["GET"]) def returnAllObservatories(): - get_all = models.Observatory.query.order_by("title").all() + get_all = ( + models.Observatory.query.join(models.ObservatoryTranslation) + .order_by(models.ObservatoryTranslation.title) + .all() + ) items = observatories_schema.dump(get_all) return jsonify(items) @@ -476,7 +480,11 @@ def deletePhotos(): @api.route("/api/communes", methods=["GET"]) def returnAllcommunes(): - get_all_communes = models.Communes.query.order_by("nom_commune").all() + get_all_communes = ( + models.Communes.query.join(models.CommunesTranslation) + .order_by(models.CommunesTranslation.nom_commune) + .all() + ) communes = models.CommunesSchema(many=True).dump(get_all_communes) return jsonify(communes), 200 diff --git a/backend/migrations/versions/d7fd422e1054_translations.py b/backend/migrations/versions/d7fd422e1054_translations.py new file mode 100644 index 00000000..4654b92a --- /dev/null +++ b/backend/migrations/versions/d7fd422e1054_translations.py @@ -0,0 +1,223 @@ +"""translations + +Revision ID: d7fd422e1054 +Revises: 4a02bd35bb30 +Create Date: 2024-10-22 12:20:06.196024 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd7fd422e1054' +down_revision = '4a02bd35bb30' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('lang', + sa.Column('id', sa.String(), nullable=False), + sa.Column('label', sa.String(), nullable=True), + sa.Column('is_published', sa.Boolean(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True, default=False), + sa.PrimaryKeyConstraint('id'), + sa.CheckConstraint( + "is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", + name="unique_default_lang" + ), + schema='geopaysages' + ) + # Insert default lang 'fr' + op.execute(sa.text(""" + INSERT INTO geopaysages.lang (id, label) + VALUES ('fr', 'Français') + """)) + + op.create_table('communes_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('nom_commune', sa.String(), nullable=True), + sa.Column('row_id', sa.String(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='communes_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.communes.code_commune'], name='commune_code_commune'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + # Insert existing communes in translation table + op.execute(sa.text(""" + INSERT INTO geopaysages.communes_translation (nom_commune, row_id, lang_id) + SELECT nom_commune, code_commune, 'fr' + FROM geopaysages.communes + """)) + + op.create_table('dico_stheme_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_stheme', sa.String(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='dico_stheme_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.dico_stheme.id_stheme'], name='stheme_id_stheme'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.dico_stheme_translation (name_stheme, row_id, lang_id) + SELECT name_stheme, id_stheme, 'fr' + FROM geopaysages.dico_stheme + """)) + + op.create_table('dico_theme_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_theme', sa.String(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='dico_theme_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.dico_theme.id_theme'], name='theme_id_theme'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.dico_theme_translation (name_theme, row_id, lang_id) + SELECT name_theme, id_theme, 'fr' + FROM geopaysages.dico_theme + """)) + + op.create_table('t_observatory_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('is_published', sa.Boolean(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='t_observatory_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.t_observatory.id'], name='observatory_id'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.t_observatory_translation (title, is_published, row_id, lang_id) + SELECT title, is_published, id, 'fr' + FROM geopaysages.t_observatory + """)) + + op.create_table('t_site_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_site', sa.String(), nullable=True), + sa.Column('desc_site', sa.String(), nullable=True), + sa.Column('legend_site', sa.String(), nullable=True), + sa.Column('publish_site', sa.Boolean(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='t_site_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.t_site.id_site'], name='site_id_site'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.t_site_translation (name_site, desc_site, legend_site, publish_site, row_id, lang_id) + SELECT name_site, desc_site, legend_site, publish_site, id_site, 'fr' + FROM geopaysages.t_site + """)) + + op.drop_column('communes', 'nom_commune', schema='geopaysages') + op.drop_column('dico_stheme', 'name_stheme', schema='geopaysages') + op.drop_column('dico_theme', 'name_theme', schema='geopaysages') + op.drop_column('t_observatory', 'title', schema='geopaysages') + op.drop_column('t_observatory', 'is_published', schema='geopaysages') + op.drop_column('t_site', 'publish_site', schema='geopaysages') + op.drop_column('t_site', 'name_site', schema='geopaysages') + op.drop_column('t_site', 'desc_site', schema='geopaysages') + op.drop_column('t_site', 'legend_site', schema='geopaysages') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('t_site', sa.Column('legend_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('desc_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('name_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('publish_site', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_observatory', sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_observatory', sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('dico_theme', sa.Column('name_theme', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('dico_stheme', sa.Column('name_stheme', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('communes', sa.Column('nom_commune', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + + + # populate + op.execute(sa.text(""" + UPDATE geopaysages.communes c + SET nom_commune = ( + SELECT ct.nom_commune + FROM geopaysages.communes_translation ct + WHERE ct.row_id = c.code_commune AND ct.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.dico_stheme d + SET name_stheme = ( + SELECT dt.name_stheme + FROM geopaysages.dico_stheme_translation dt + WHERE d.id_stheme = dt.row_id AND dt.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.dico_theme d + SET name_theme = ( + SELECT dt.name_theme + FROM geopaysages.dico_theme_translation dt + WHERE d.id_theme = dt.row_id AND dt.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.t_observatory o + SET + title = ( + SELECT ot.title + FROM geopaysages.t_observatory_translation ot + WHERE o.id = ot.row_id AND ot.lang_id = 'fr' + ), + is_published = ( + SELECT ot.is_published + FROM geopaysages.t_observatory_translation ot + WHERE o.id = ot.row_id AND ot.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.t_site s + SET + legend_site = ( + SELECT st.legend_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), + desc_site = ( + SELECT st.desc_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), + name_site = ( + SELECT st.name_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), + publish_site = ( + SELECT st.publish_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ) + """)) + + op.drop_table('t_site_translation', schema='geopaysages') + op.drop_table('t_observatory_translation', schema='geopaysages') + op.drop_table('dico_theme_translation', schema='geopaysages') + op.drop_table('dico_stheme_translation', schema='geopaysages') + op.drop_table('communes_translation', schema='geopaysages') + op.drop_table('lang', schema='geopaysages') + # ### end Alembic commands ### diff --git a/backend/models.py b/backend/models.py index 820c0eea..8c02fd35 100644 --- a/backend/models.py +++ b/backend/models.py @@ -19,6 +19,35 @@ class Conf(db.Model): value = db.Column(db.String) +class Lang(db.Model): + __tablename__ = "lang" + __table_args__ = ( + {"schema": "geopaysages"}, + db.CheckConstraint( + "is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", + name="unique_default_lang", + ), + ) + + id = db.Column(db.String, primary_key=True) + label = db.Column(db.String) + is_published = db.Column(db.Boolean) + is_default = db.Column(db.Boolean, default=False) + observatory_translations = db.relationship( + "ObservatoryTranslation", back_populates="lang" + ) + site_translations = db.relationship("TSiteTranslation", back_populates="lang") + dico_stheme_translations = db.relationship( + "DicoSthemeTranslation", back_populates="lang" + ) + dico_theme_translations = db.relationship( + "DicoThemeTranslation", back_populates="lang" + ) + communes_translations = db.relationship( + "CommunesTranslation", back_populates="lang" + ) + + class ComparatorEnum(Enum): sidebyside = "sidebyside" split = "split" @@ -29,14 +58,34 @@ class Observatory(db.Model): __table_args__ = {"schema": "geopaysages"} id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) - title = db.Column(db.String) ref = db.Column(db.String) color = db.Column(db.String) thumbnail = db.Column(db.String) logo = db.Column(db.String) comparator = db.Column(db.Enum(ComparatorEnum, name="comparator_enum")) geom = db.Column(Geometry(geometry_type="MULTIPOLYGON", srid=4326)) + translations = db.relationship( + "ObservatoryTranslation", back_populates="row", lazy=True + ) + + +class ObservatoryTranslation(db.Model): + __tablename__ = "t_observatory_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + title = db.Column(db.String) is_published = db.Column(db.Boolean) + row_id = db.Column( + db.ForeignKey("geopaysages.t_observatory.id", name="observatory_id") + ) + row = db.relationship("Observatory", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="t_observatory_translation_fk_lang") + ) + lang = db.relationship( + "Lang", primaryjoin="ObservatoryTranslation.lang_id == Lang.id" + ) class TSite(db.Model): @@ -50,21 +99,35 @@ class TSite(db.Model): observatory = db.relationship( "Observatory", primaryjoin="TSite.id_observatory == Observatory.id" ) - name_site = db.Column(db.String) ref_site = db.Column(db.String) - desc_site = db.Column(db.String) - legend_site = db.Column(db.String) testim_site = db.Column(db.String) code_city_site = db.Column(db.String) alti_site = db.Column(db.Integer) path_file_guide_site = db.Column(db.String) - publish_site = db.Column(db.Boolean) geom = db.Column(Geometry(geometry_type="POINT", srid=4326)) main_photo = db.Column(db.Integer) main_theme_id = db.Column(db.ForeignKey("geopaysages.dico_theme.id_theme")) main_theme = db.relationship( "DicoTheme", primaryjoin="TSite.main_theme_id == DicoTheme.id_theme" ) + translations = db.relationship("TSiteTranslation", back_populates="row", lazy=True) + + +class TSiteTranslation(db.Model): + __tablename__ = "t_site_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + name_site = db.Column(db.String) + desc_site = db.Column(db.String) + legend_site = db.Column(db.String) + publish_site = db.Column(db.Boolean) + row_id = db.Column(db.ForeignKey("geopaysages.t_site.id_site", name="site_id_site")) + row = db.relationship("TSite", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="t_site_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="site_translations") class CorSiteSthemeTheme(db.Model): @@ -143,7 +206,25 @@ class DicoStheme(db.Model): id_stheme = db.Column( db.Integer, primary_key=True, server_default=db.FetchedValue() ) + translations = db.relationship( + "DicoSthemeTranslation", back_populates="row", lazy=True + ) + + +class DicoSthemeTranslation(db.Model): + __tablename__ = "dico_stheme_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) name_stheme = db.Column(db.String) + row_id = db.Column( + db.ForeignKey("geopaysages.dico_stheme.id_stheme", name="stheme_id_stheme") + ) + row = db.relationship("DicoStheme", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="dico_stheme_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="dico_stheme_translations") class DicoTheme(db.Model): @@ -151,8 +232,26 @@ class DicoTheme(db.Model): __table_args__ = {"schema": "geopaysages"} id_theme = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) - name_theme = db.Column(db.String) icon = db.Column(db.String) + translations = db.relationship( + "DicoThemeTranslation", back_populates="row", lazy=True + ) + + +class DicoThemeTranslation(db.Model): + __tablename__ = "dico_theme_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) + name_theme = db.Column(db.String) + row_id = db.Column( + db.ForeignKey("geopaysages.dico_theme.id_theme", name="theme_id_theme") + ) + row = db.relationship("DicoTheme", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="dico_theme_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="dico_theme_translations") class TRole(db.Model): @@ -233,7 +332,25 @@ class Communes(db.Model): code_commune = db.Column( db.String, primary_key=True, server_default=db.FetchedValue() ) + translations = db.relationship( + "CommunesTranslation", back_populates="row", lazy=True + ) + + +class CommunesTranslation(db.Model): + __tablename__ = "communes_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) nom_commune = db.Column(db.String) + row_id = db.Column( + db.ForeignKey("geopaysages.communes.code_commune", name="commune_code_commune") + ) + row = db.relationship("Communes", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="communes_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="communes_translations") class GeographySerializationField(fields.String): @@ -266,12 +383,68 @@ def _deserialize(self, value, attr, data): # schemas# +class TranslationSchema(ma.SQLAlchemyAutoSchema): + class Meta: + fields = ("lang_id", "title", "is_published") + + +class LangSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Lang + fields = ("id", "label") + + +class CommunesTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + + class Meta: + model = CommunesTranslation + fields = ("nom_commune", "lang_id", "lang") + + +class ObservatoryTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + + class Meta: + model = ObservatoryTranslation + fields = ("title", "is_published", "lang_id") + + +class TSiteTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + + class Meta: + model = TSiteTranslation + fields = ("name_site", "desc_site", "legend_site", "publish_site", "lang_id") + + +class DicoThemeTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + + class Meta: + model = DicoThemeTranslation + fields = ("name_theme", "lang_id", "lang") + + +class DicoSthemeTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + + class Meta: + model = DicoSthemeTranslation + fields = ("name_stheme", "lang_id", "lang") + + class DicoThemeSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(DicoThemeTranslationSchema, many=True) + class Meta: - fields = ("id_theme", "name_theme", "icon") + model = DicoTheme + fields = ("id_theme", "icon", "translations") class DicoSthemeSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(DicoSthemeTranslationSchema, many=True) + class Meta: model = DicoStheme include_relationships = True @@ -311,6 +484,7 @@ class Meta: class ObservatorySchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(ObservatoryTranslationSchema, many=True) comparator = EnumField(ComparatorEnum, by_value=True) geom = fields.Method("geomSerialize") @@ -327,10 +501,7 @@ class Meta: include_relationships = True -class ObservatorySchemaFull(ma.SQLAlchemyAutoSchema): - comparator = EnumField(ComparatorEnum, by_value=True) - geom = fields.Method("geomSerialize") - +class ObservatorySchemaFull(ObservatorySchema): @staticmethod def geomSerialize(obj): if obj.geom is None: @@ -338,14 +509,9 @@ def geomSerialize(obj): p = to_shape(obj.geom) return p.wkt - class Meta: - model = Observatory - include_relationships = True - -class ObservatorySchemaLite(ma.SQLAlchemyAutoSchema): +class ObservatorySchemaLite(ObservatorySchema): comparator = EnumField(ComparatorEnum, by_value=False) - geom = fields.Method("geomSerialize") @staticmethod def geomSerialize(obj): @@ -355,17 +521,14 @@ def geomSerialize(obj): s = p.simplify(0.001, preserve_topology=True) return s.wkt - class Meta: - model = Observatory - include_relationships = True - class TSiteSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(TSiteTranslationSchema, many=True) geom = GeographySerializationField(attribute="geom") observatory = ma.Nested( ObservatorySchema, only=["id", "title", "ref", "color", "logo"] ) - main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "name_theme", "icon"]) + main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) class Meta: model = TSite @@ -374,5 +537,8 @@ class Meta: class CommunesSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(CommunesTranslationSchema, many=True) + class Meta: model = Communes + include_relationships = True From 4d87188714a21814e9bd9472032a1028a78b5c04 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 12:49:06 +0200 Subject: [PATCH 007/107] feat: add is_published and is_default unique to lang table --- .../versions/d7fd422e1054_translations.py | 20 +++++++++++-------- backend/models.py | 8 ++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/migrations/versions/d7fd422e1054_translations.py b/backend/migrations/versions/d7fd422e1054_translations.py index 4654b92a..56636922 100644 --- a/backend/migrations/versions/d7fd422e1054_translations.py +++ b/backend/migrations/versions/d7fd422e1054_translations.py @@ -24,16 +24,13 @@ def upgrade(): sa.Column('is_published', sa.Boolean(), nullable=True), sa.Column('is_default', sa.Boolean(), nullable=True, default=False), sa.PrimaryKeyConstraint('id'), - sa.CheckConstraint( - "is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", - name="unique_default_lang" - ), + sa.UniqueConstraint('is_default', name='uq_default_lang'), schema='geopaysages' ) # Insert default lang 'fr' op.execute(sa.text(""" - INSERT INTO geopaysages.lang (id, label) - VALUES ('fr', 'Français') + INSERT INTO geopaysages.lang (id, label, is_published, is_default) + VALUES ('fr', 'Français', true, true) """)) op.create_table('communes_translation', @@ -106,6 +103,7 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('name_site', sa.String(), nullable=True), sa.Column('desc_site', sa.String(), nullable=True), + sa.Column('testim_site', sa.String(), nullable=True), sa.Column('legend_site', sa.String(), nullable=True), sa.Column('publish_site', sa.Boolean(), nullable=True), sa.Column('row_id', sa.Integer(), nullable=True), @@ -116,8 +114,8 @@ def upgrade(): schema='geopaysages' ) op.execute(sa.text(""" - INSERT INTO geopaysages.t_site_translation (name_site, desc_site, legend_site, publish_site, row_id, lang_id) - SELECT name_site, desc_site, legend_site, publish_site, id_site, 'fr' + INSERT INTO geopaysages.t_site_translation (name_site, desc_site, testim_site, legend_site, publish_site, row_id, lang_id) + SELECT name_site, desc_site, testim_site, legend_site, publish_site, id_site, 'fr' FROM geopaysages.t_site """)) @@ -137,6 +135,7 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column('t_site', sa.Column('legend_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_site', sa.Column('desc_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('testim_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_site', sa.Column('name_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_site', sa.Column('publish_site', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_observatory', sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') @@ -202,6 +201,11 @@ def downgrade(): FROM geopaysages.t_site_translation st WHERE s.id_site = st.row_id AND st.lang_id = 'fr' ), + testim_site = ( + SELECT st.testim_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), name_site = ( SELECT st.name_site FROM geopaysages.t_site_translation st diff --git a/backend/models.py b/backend/models.py index 8c02fd35..2fd37dea 100644 --- a/backend/models.py +++ b/backend/models.py @@ -23,10 +23,6 @@ class Lang(db.Model): __tablename__ = "lang" __table_args__ = ( {"schema": "geopaysages"}, - db.CheckConstraint( - "is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", - name="unique_default_lang", - ), ) id = db.Column(db.String, primary_key=True) @@ -100,7 +96,6 @@ class TSite(db.Model): "Observatory", primaryjoin="TSite.id_observatory == Observatory.id" ) ref_site = db.Column(db.String) - testim_site = db.Column(db.String) code_city_site = db.Column(db.String) alti_site = db.Column(db.Integer) path_file_guide_site = db.Column(db.String) @@ -120,6 +115,7 @@ class TSiteTranslation(db.Model): id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) name_site = db.Column(db.String) desc_site = db.Column(db.String) + testim_site = db.Column(db.String) legend_site = db.Column(db.String) publish_site = db.Column(db.Boolean) row_id = db.Column(db.ForeignKey("geopaysages.t_site.id_site", name="site_id_site")) @@ -391,7 +387,7 @@ class Meta: class LangSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Lang - fields = ("id", "label") + fields = ("id", "label", "is_published", "is_default") class CommunesTranslationSchema(ma.SQLAlchemyAutoSchema): From 206c8a3024220bf85cd80a82ea69bdd59d6dd55f Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 13:49:06 +0200 Subject: [PATCH 008/107] feat: add languages route --- backend/api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/api.py b/backend/api.py index 5c002890..255227ff 100644 --- a/backend/api.py +++ b/backend/api.py @@ -489,7 +489,14 @@ def returnAllcommunes(): return jsonify(communes), 200 -@api.route("/api/logout", methods=["GET"]) +@api.route('/api/languages', methods=['GET']) +def returnAllLanguages(): + get_all_languages = models.Lang.query.all() + languages = models.LangSchema(many=True).dump(get_all_languages) + return jsonify(languages), 200 + + +@api.route('/api/logout', methods=['GET']) def logout(): resp = Response("", 200) resp.delete_cookie("token") From 408eebe129d4fcf707e7a8f649ac386ac5f89bf8 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 15:50:13 +0200 Subject: [PATCH 009/107] chore: remove unused marshmallow schema --- backend/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/models.py b/backend/models.py index 2fd37dea..531b1669 100644 --- a/backend/models.py +++ b/backend/models.py @@ -379,11 +379,6 @@ def _deserialize(self, value, attr, data): # schemas# -class TranslationSchema(ma.SQLAlchemyAutoSchema): - class Meta: - fields = ("lang_id", "title", "is_published") - - class LangSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Lang From bd5b175a56f1b988621a5bbde1ebd2053488e355 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 18:01:55 +0200 Subject: [PATCH 010/107] feat: wip observatories api endpoints with translations --- backend/api.py | 70 +++++++++++++++++++++++++++++++++++++++++++---- backend/models.py | 8 ++++-- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/backend/api.py b/backend/api.py index 255227ff..0db9465d 100644 --- a/backend/api.py +++ b/backend/api.py @@ -94,10 +94,37 @@ def returnAllObservatories(): def postObservatory(): try: data = dict(request.get_json()) + translations_data = data.pop("translations", []) db_obj = models.Observatory(**data) + db.session.add(db_obj) db.session.commit() + + translations = [] + for translate in translations_data: + if "lang_id" not in translate or "title" not in translate: + return ( + jsonify( + { + "error": "Each translation must include 'lang_id' and 'title'." + } + ), + 400, + ) + + translation_obj = models.ObservatoryTranslation( + title=translate["title"], + is_published=translate["is_published"], + lang_id=translate["lang_id"], + row_id=db_obj.id, + ) + translations.append(translation_obj) + + db.session.add_all(translations) + db.session.commit() + except Exception as exception: + db.session.rollback() print(exception) return str(exception), 400 @@ -116,16 +143,47 @@ def returnObservatoryById(id): @api.route("/api/observatories/", methods=["PATCH"]) -@fnauth.check_auth(2) +# @fnauth.check_auth(2) def patchObservatory(id): try: - rows = models.Observatory.query.filter_by(id=id) - if not rows.count(): + observatory = models.Observatory.query.filter_by(id=id).first() + if not observatory: abort(404) data = request.get_json() - rows.update(data) + translations_data = data.pop("translations", []) + + for key, value in data.items(): + setattr(observatory, key, value) + db.session.commit() + + existing_translations = {t.lang_id: t for t in observatory.translations} + for translate in translations_data: + if "lang_id" not in translate or "title" not in translate: + return ( + jsonify( + { + "error": "Each translation must include 'lang_id' and 'title'." + } + ), + 400, + ) + if translate["lang_id"] in existing_translations: + translation_obj = existing_translations[translate["lang_id"]] + translation_obj.title = translate["title"] + translation_obj.is_published = translate["is_published"] + else: + new_translation = models.ObservatoryTranslation( + title=translate["title"], + is_published=translate["is_published"], + lang_id=translate["lang_id"], + row_id=observatory.id, + ) + db.session.add(new_translation) + db.session.commit() + except Exception as exception: + db.session.rollback() return str(exception), 400 row = models.Observatory.query.filter_by(id=id).first() dict = observatory_schema_full.dump(row) @@ -489,14 +547,14 @@ def returnAllcommunes(): return jsonify(communes), 200 -@api.route('/api/languages', methods=['GET']) +@api.route("/api/languages", methods=["GET"]) def returnAllLanguages(): get_all_languages = models.Lang.query.all() languages = models.LangSchema(many=True).dump(get_all_languages) return jsonify(languages), 200 -@api.route('/api/logout', methods=['GET']) +@api.route("/api/logout", methods=["GET"]) def logout(): resp = Response("", 200) resp.delete_cookie("token") diff --git a/backend/models.py b/backend/models.py index 531b1669..a06def68 100644 --- a/backend/models.py +++ b/backend/models.py @@ -21,14 +21,16 @@ class Conf(db.Model): class Lang(db.Model): __tablename__ = "lang" - __table_args__ = ( - {"schema": "geopaysages"}, - ) + __table_args__ = ({"schema": "geopaysages"},) id = db.Column(db.String, primary_key=True) label = db.Column(db.String) is_published = db.Column(db.Boolean) is_default = db.Column(db.Boolean, default=False) + __table_args__ = ( + db.UniqueConstraint("is_default", name="uq_default_lang"), + {"schema": "geopaysages"}, + ) observatory_translations = db.relationship( "ObservatoryTranslation", back_populates="lang" ) From f840d8c734270e3aa1d501b725cf9e177db0c8df Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 18:05:33 +0200 Subject: [PATCH 011/107] chore: minor change --- backend/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api.py b/backend/api.py index 0db9465d..d913397b 100644 --- a/backend/api.py +++ b/backend/api.py @@ -143,7 +143,7 @@ def returnObservatoryById(id): @api.route("/api/observatories/", methods=["PATCH"]) -# @fnauth.check_auth(2) +@fnauth.check_auth(2) def patchObservatory(id): try: observatory = models.Observatory.query.filter_by(id=id).first() From 863e84cf85d5ff15f6d6b4ec7722e6d69632c3c3 Mon Sep 17 00:00:00 2001 From: jules-jean-louis1 Date: Wed, 23 Oct 2024 22:01:49 +0200 Subject: [PATCH 012/107] fix: drop column on upgrade translations migration --- backend/api.py | 38 +- .../versions/d7fd422e1054_translations.py | 361 ++++++++++++------ 2 files changed, 271 insertions(+), 128 deletions(-) diff --git a/backend/api.py b/backend/api.py index d913397b..124cd0d5 100644 --- a/backend/api.py +++ b/backend/api.py @@ -369,12 +369,40 @@ def deleteSite(id_site): @api.route("/api/addSite", methods=["POST"]) -@fnauth.check_auth(2) +# @fnauth.check_auth(2) def add_site(): - data = dict(request.get_json()) - site = models.TSite(**data) - db.session.add(site) - db.session.commit() + try: + data = dict(request.get_json()) + transalations_data = data.pop("translations", []) + site = models.TSite(**data) + db.session.add(site) + db.session.commit() + + translations = [] + for translate in transalations_data: + if "lang_id" not in translate: + return ( + jsonify({"error": "Each translation must include 'lang_id'."}), + 400, + ) + translation_obj = models.TSiteTranslation( + name_site=translate["name_site"], + desc_site=translate["desc_site"], + testim_site=translate["testim_site"], + legend_site=translate["legend_site"], + publish_site=translate["publish_site"], + lang_id=translate["lang_id"], + row_id=site.id_site, + ) + translations.append(translation_obj) + + db.session.add_all(translations) + db.session.commit() + + except Exception as exception: + db.session.rollback() + print(exception) + return str(exception), 400 return jsonify(id_site=site.id_site), 200 diff --git a/backend/migrations/versions/d7fd422e1054_translations.py b/backend/migrations/versions/d7fd422e1054_translations.py index 56636922..8540773a 100644 --- a/backend/migrations/versions/d7fd422e1054_translations.py +++ b/backend/migrations/versions/d7fd422e1054_translations.py @@ -5,175 +5,284 @@ Create Date: 2024-10-22 12:20:06.196024 """ + from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'd7fd422e1054' -down_revision = '4a02bd35bb30' +revision = "d7fd422e1054" +down_revision = "4a02bd35bb30" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('lang', - sa.Column('id', sa.String(), nullable=False), - sa.Column('label', sa.String(), nullable=True), - sa.Column('is_published', sa.Boolean(), nullable=True), - sa.Column('is_default', sa.Boolean(), nullable=True, default=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('is_default', name='uq_default_lang'), - schema='geopaysages' + op.create_table( + "lang", + sa.Column("id", sa.String(), nullable=False), + sa.Column("label", sa.String(), nullable=True), + sa.Column("is_published", sa.Boolean(), nullable=True), + sa.Column("is_default", sa.Boolean(), nullable=True, default=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("is_default", name="uq_default_lang"), + schema="geopaysages", ) # Insert default lang 'fr' - op.execute(sa.text(""" + op.execute( + sa.text( + """ INSERT INTO geopaysages.lang (id, label, is_published, is_default) VALUES ('fr', 'Français', true, true) - """)) - - op.create_table('communes_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('nom_commune', sa.String(), nullable=True), - sa.Column('row_id', sa.String(), nullable=True), - sa.Column('lang_id', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='communes_translation_fk_lang'), - sa.ForeignKeyConstraint(['row_id'], ['geopaysages.communes.code_commune'], name='commune_code_commune'), - sa.PrimaryKeyConstraint('id'), - schema='geopaysages' + """ + ) + ) + + op.create_table( + "communes_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("nom_commune", sa.String(), nullable=True), + sa.Column("row_id", sa.String(), nullable=True), + sa.Column("lang_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["lang_id"], ["geopaysages.lang.id"], name="communes_translation_fk_lang" + ), + sa.ForeignKeyConstraint( + ["row_id"], + ["geopaysages.communes.code_commune"], + name="commune_code_commune", + ), + sa.PrimaryKeyConstraint("id"), + schema="geopaysages", ) # Insert existing communes in translation table - op.execute(sa.text(""" + op.execute( + sa.text( + """ INSERT INTO geopaysages.communes_translation (nom_commune, row_id, lang_id) SELECT nom_commune, code_commune, 'fr' FROM geopaysages.communes - """)) - - op.create_table('dico_stheme_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name_stheme', sa.String(), nullable=True), - sa.Column('row_id', sa.Integer(), nullable=True), - sa.Column('lang_id', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='dico_stheme_translation_fk_lang'), - sa.ForeignKeyConstraint(['row_id'], ['geopaysages.dico_stheme.id_stheme'], name='stheme_id_stheme'), - sa.PrimaryKeyConstraint('id'), - schema='geopaysages' - ) - op.execute(sa.text(""" + """ + ) + ) + + op.create_table( + "dico_stheme_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name_stheme", sa.String(), nullable=True), + sa.Column("row_id", sa.Integer(), nullable=True), + sa.Column("lang_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["lang_id"], ["geopaysages.lang.id"], name="dico_stheme_translation_fk_lang" + ), + sa.ForeignKeyConstraint( + ["row_id"], ["geopaysages.dico_stheme.id_stheme"], name="stheme_id_stheme" + ), + sa.PrimaryKeyConstraint("id"), + schema="geopaysages", + ) + op.execute( + sa.text( + """ INSERT INTO geopaysages.dico_stheme_translation (name_stheme, row_id, lang_id) SELECT name_stheme, id_stheme, 'fr' FROM geopaysages.dico_stheme - """)) - - op.create_table('dico_theme_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name_theme', sa.String(), nullable=True), - sa.Column('row_id', sa.Integer(), nullable=True), - sa.Column('lang_id', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='dico_theme_translation_fk_lang'), - sa.ForeignKeyConstraint(['row_id'], ['geopaysages.dico_theme.id_theme'], name='theme_id_theme'), - sa.PrimaryKeyConstraint('id'), - schema='geopaysages' - ) - op.execute(sa.text(""" + """ + ) + ) + + op.create_table( + "dico_theme_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name_theme", sa.String(), nullable=True), + sa.Column("row_id", sa.Integer(), nullable=True), + sa.Column("lang_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["lang_id"], ["geopaysages.lang.id"], name="dico_theme_translation_fk_lang" + ), + sa.ForeignKeyConstraint( + ["row_id"], ["geopaysages.dico_theme.id_theme"], name="theme_id_theme" + ), + sa.PrimaryKeyConstraint("id"), + schema="geopaysages", + ) + op.execute( + sa.text( + """ INSERT INTO geopaysages.dico_theme_translation (name_theme, row_id, lang_id) SELECT name_theme, id_theme, 'fr' FROM geopaysages.dico_theme - """)) - - op.create_table('t_observatory_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(), nullable=True), - sa.Column('is_published', sa.Boolean(), nullable=True), - sa.Column('row_id', sa.Integer(), nullable=True), - sa.Column('lang_id', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='t_observatory_translation_fk_lang'), - sa.ForeignKeyConstraint(['row_id'], ['geopaysages.t_observatory.id'], name='observatory_id'), - sa.PrimaryKeyConstraint('id'), - schema='geopaysages' - ) - op.execute(sa.text(""" + """ + ) + ) + + op.create_table( + "t_observatory_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("is_published", sa.Boolean(), nullable=True), + sa.Column("row_id", sa.Integer(), nullable=True), + sa.Column("lang_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["lang_id"], + ["geopaysages.lang.id"], + name="t_observatory_translation_fk_lang", + ), + sa.ForeignKeyConstraint( + ["row_id"], ["geopaysages.t_observatory.id"], name="observatory_id" + ), + sa.PrimaryKeyConstraint("id"), + schema="geopaysages", + ) + op.execute( + sa.text( + """ INSERT INTO geopaysages.t_observatory_translation (title, is_published, row_id, lang_id) SELECT title, is_published, id, 'fr' FROM geopaysages.t_observatory - """)) - - op.create_table('t_site_translation', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name_site', sa.String(), nullable=True), - sa.Column('desc_site', sa.String(), nullable=True), - sa.Column('testim_site', sa.String(), nullable=True), - sa.Column('legend_site', sa.String(), nullable=True), - sa.Column('publish_site', sa.Boolean(), nullable=True), - sa.Column('row_id', sa.Integer(), nullable=True), - sa.Column('lang_id', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='t_site_translation_fk_lang'), - sa.ForeignKeyConstraint(['row_id'], ['geopaysages.t_site.id_site'], name='site_id_site'), - sa.PrimaryKeyConstraint('id'), - schema='geopaysages' - ) - op.execute(sa.text(""" + """ + ) + ) + + op.create_table( + "t_site_translation", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name_site", sa.String(), nullable=True), + sa.Column("desc_site", sa.String(), nullable=True), + sa.Column("testim_site", sa.String(), nullable=True), + sa.Column("legend_site", sa.String(), nullable=True), + sa.Column("publish_site", sa.Boolean(), nullable=True), + sa.Column("row_id", sa.Integer(), nullable=True), + sa.Column("lang_id", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["lang_id"], ["geopaysages.lang.id"], name="t_site_translation_fk_lang" + ), + sa.ForeignKeyConstraint( + ["row_id"], ["geopaysages.t_site.id_site"], name="site_id_site" + ), + sa.PrimaryKeyConstraint("id"), + schema="geopaysages", + ) + op.execute( + sa.text( + """ INSERT INTO geopaysages.t_site_translation (name_site, desc_site, testim_site, legend_site, publish_site, row_id, lang_id) SELECT name_site, desc_site, testim_site, legend_site, publish_site, id_site, 'fr' FROM geopaysages.t_site - """)) - - op.drop_column('communes', 'nom_commune', schema='geopaysages') - op.drop_column('dico_stheme', 'name_stheme', schema='geopaysages') - op.drop_column('dico_theme', 'name_theme', schema='geopaysages') - op.drop_column('t_observatory', 'title', schema='geopaysages') - op.drop_column('t_observatory', 'is_published', schema='geopaysages') - op.drop_column('t_site', 'publish_site', schema='geopaysages') - op.drop_column('t_site', 'name_site', schema='geopaysages') - op.drop_column('t_site', 'desc_site', schema='geopaysages') - op.drop_column('t_site', 'legend_site', schema='geopaysages') + """ + ) + ) + + op.drop_column("communes", "nom_commune", schema="geopaysages") + op.drop_column("dico_stheme", "name_stheme", schema="geopaysages") + op.drop_column("dico_theme", "name_theme", schema="geopaysages") + op.drop_column("t_observatory", "title", schema="geopaysages") + op.drop_column("t_observatory", "is_published", schema="geopaysages") + op.drop_column("t_site", "publish_site", schema="geopaysages") + op.drop_column("t_site", "name_site", schema="geopaysages") + op.drop_column("t_site", "desc_site", schema="geopaysages") + op.drop_column("t_site", "legend_site", schema="geopaysages") + op.drop_column("t_site", "testim_site", schema="geopaysages") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('t_site', sa.Column('legend_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('t_site', sa.Column('desc_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('t_site', sa.Column('testim_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('t_site', sa.Column('name_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('t_site', sa.Column('publish_site', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('t_observatory', sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('t_observatory', sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('dico_theme', sa.Column('name_theme', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('dico_stheme', sa.Column('name_stheme', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - op.add_column('communes', sa.Column('nom_commune', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') - + op.add_column( + "t_site", + sa.Column("legend_site", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_site", + sa.Column("desc_site", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_site", + sa.Column("testim_site", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_site", + sa.Column("name_site", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_site", + sa.Column("publish_site", sa.BOOLEAN(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_observatory", + sa.Column("is_published", sa.BOOLEAN(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "t_observatory", + sa.Column("title", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "dico_theme", + sa.Column("name_theme", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "dico_stheme", + sa.Column("name_stheme", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) + op.add_column( + "communes", + sa.Column("nom_commune", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="geopaysages", + ) # populate - op.execute(sa.text(""" + op.execute( + sa.text( + """ UPDATE geopaysages.communes c SET nom_commune = ( SELECT ct.nom_commune FROM geopaysages.communes_translation ct WHERE ct.row_id = c.code_commune AND ct.lang_id = 'fr' ) - """)) - - op.execute(sa.text(""" + """ + ) + ) + + op.execute( + sa.text( + """ UPDATE geopaysages.dico_stheme d SET name_stheme = ( SELECT dt.name_stheme FROM geopaysages.dico_stheme_translation dt WHERE d.id_stheme = dt.row_id AND dt.lang_id = 'fr' ) - """)) - - op.execute(sa.text(""" + """ + ) + ) + + op.execute( + sa.text( + """ UPDATE geopaysages.dico_theme d SET name_theme = ( SELECT dt.name_theme FROM geopaysages.dico_theme_translation dt WHERE d.id_theme = dt.row_id AND dt.lang_id = 'fr' ) - """)) - - op.execute(sa.text(""" + """ + ) + ) + + op.execute( + sa.text( + """ UPDATE geopaysages.t_observatory o SET title = ( @@ -186,9 +295,13 @@ def downgrade(): FROM geopaysages.t_observatory_translation ot WHERE o.id = ot.row_id AND ot.lang_id = 'fr' ) - """)) - - op.execute(sa.text(""" + """ + ) + ) + + op.execute( + sa.text( + """ UPDATE geopaysages.t_site s SET legend_site = ( @@ -216,12 +329,14 @@ def downgrade(): FROM geopaysages.t_site_translation st WHERE s.id_site = st.row_id AND st.lang_id = 'fr' ) - """)) - - op.drop_table('t_site_translation', schema='geopaysages') - op.drop_table('t_observatory_translation', schema='geopaysages') - op.drop_table('dico_theme_translation', schema='geopaysages') - op.drop_table('dico_stheme_translation', schema='geopaysages') - op.drop_table('communes_translation', schema='geopaysages') - op.drop_table('lang', schema='geopaysages') + """ + ) + ) + + op.drop_table("t_site_translation", schema="geopaysages") + op.drop_table("t_observatory_translation", schema="geopaysages") + op.drop_table("dico_theme_translation", schema="geopaysages") + op.drop_table("dico_stheme_translation", schema="geopaysages") + op.drop_table("communes_translation", schema="geopaysages") + op.drop_table("lang", schema="geopaysages") # ### end Alembic commands ### From 122e0dede2ddf25a49e1cc116fcd39d8551107f3 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Thu, 24 Oct 2024 09:22:06 +0200 Subject: [PATCH 013/107] chore: minor change --- backend/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api.py b/backend/api.py index 124cd0d5..4e0d1ce5 100644 --- a/backend/api.py +++ b/backend/api.py @@ -369,7 +369,7 @@ def deleteSite(id_site): @api.route("/api/addSite", methods=["POST"]) -# @fnauth.check_auth(2) +@fnauth.check_auth(2) def add_site(): try: data = dict(request.get_json()) From 11e93de3b29cce53f0b7ba972d75f89abba47d4f Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Thu, 24 Oct 2024 14:57:54 +0200 Subject: [PATCH 014/107] feat: update all routes to align with new database models and added translations --- backend/api.py | 99 ++++++++++++++++++++++++++++++++++------------- backend/models.py | 3 +- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/backend/api.py b/backend/api.py index 4e0d1ce5..9c14ab60 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,5 +1,4 @@ from flask import ( - Flask, request, Blueprint, Response, @@ -102,13 +101,9 @@ def postObservatory(): translations = [] for translate in translations_data: - if "lang_id" not in translate or "title" not in translate: + if "lang_id" not in translate: return ( - jsonify( - { - "error": "Each translation must include 'lang_id' and 'title'." - } - ), + jsonify({"error": "Each translation must include 'lang_id'."}), 400, ) @@ -152,26 +147,24 @@ def patchObservatory(id): data = request.get_json() translations_data = data.pop("translations", []) - for key, value in data.items(): - setattr(observatory, key, value) + models.Observatory.query.filter_by(id=id).update(data) db.session.commit() - existing_translations = {t.lang_id: t for t in observatory.translations} for translate in translations_data: - if "lang_id" not in translate or "title" not in translate: + if "lang_id" not in translate: return ( - jsonify( - { - "error": "Each translation must include 'lang_id' and 'title'." - } - ), + jsonify({"error": "Each translation must include 'lang_id'."}), 400, ) - if translate["lang_id"] in existing_translations: - translation_obj = existing_translations[translate["lang_id"]] - translation_obj.title = translate["title"] - translation_obj.is_published = translate["is_published"] - else: + result = models.ObservatoryTranslation.query.filter_by( + row_id=observatory.id, lang_id=translate["lang_id"] + ).update( + { + "title": translate["title"], + "is_published": translate["is_published"], + } + ) + if result == 0: new_translation = models.ObservatoryTranslation( title=translate["title"], is_published=translate["is_published"], @@ -224,7 +217,11 @@ def patchObservatoryImage(id): @api.route("/api/sites", methods=["GET"]) def returnAllSites(): dbconf = utils.getDbConf() - get_all_sites = models.TSite.query.order_by(dbconf["default_sort_sites"]).all() + get_all_sites = ( + models.TSite.query.join(models.TSiteTranslation) + .order_by(dbconf["default_sort_sites"]) + .all() + ) sites = site_schema.dump(get_all_sites) for site in sites: if len(site.get("t_photos")) > 0: @@ -354,6 +351,7 @@ def deleteSite(id_site): photos = models.TPhoto.query.filter_by(id_site=id_site).all() photos = photo_schema.dump(photos) models.TPhoto.query.filter_by(id_site=id_site).delete() + models.TSiteTranslation.query.filter_by(row_id=id_site).delete() site = models.TSite.query.filter_by(id_site=id_site).delete() for photo in photos: photo_name = photo.get("path_file_photo") @@ -410,10 +408,59 @@ def add_site(): @api.route("/api/updateSite", methods=["PATCH"]) @fnauth.check_auth(2) def update_site(): - site = request.get_json() - models.CorSiteSthemeTheme.query.filter_by(id_site=site.get("id_site")).delete() - models.TSite.query.filter_by(id_site=site.get("id_site")).update(site) - db.session.commit() + try: + site_data = request.get_json() + + site_id = site_data.get("id_site") + if not site_id: + return jsonify({"error": "Missing 'id_site'."}), 400 + + translations_data = site_data.pop("translations", []) + + models.CorSiteSthemeTheme.query.filter_by( + id_site=site_data.get("id_site") + ).delete() + models.TSite.query.filter_by(id_site=site_id).update(site_data) + db.session.commit() + + for translate in translations_data: + if "lang_id" not in translate: + return ( + jsonify({"error": "Each translation must include 'lang_id'."}), + 400, + ) + + result = models.TSiteTranslation.query.filter_by( + row_id=site_id, lang_id=translate["lang_id"] + ).update( + { + "name_site": translate["name_site"], + "desc_site": translate["desc_site"], + "testim_site": translate.get("testim_site"), + "legend_site": translate["legend_site"], + "publish_site": translate["publish_site"], + } + ) + + if result == 0: + new_translation = models.TSiteTranslation( + row_id=site_id, + lang_id=translate["lang_id"], + name_site=translate["name_site"], + desc_site=translate["desc_site"], + testim_site=translate.get("testim_site"), + legend_site=translate["legend_site"], + publish_site=translate["publish_site"], + ) + db.session.add(new_translation) + + db.session.commit() + + except Exception as exception: + db.session.rollback() + print(exception) + return str(exception), 400 + return jsonify("site updated successfully"), 200 diff --git a/backend/models.py b/backend/models.py index a06def68..252f4f37 100644 --- a/backend/models.py +++ b/backend/models.py @@ -519,7 +519,8 @@ class TSiteSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(TSiteTranslationSchema, many=True) geom = GeographySerializationField(attribute="geom") observatory = ma.Nested( - ObservatorySchema, only=["id", "title", "ref", "color", "logo"] + ObservatorySchema, + only=["id", "translations", "ref", "color", "logo"], ) main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) From 963057a64dbe06b418bddf0fcbf847f6b33b6c35 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Thu, 24 Oct 2024 17:31:34 +0200 Subject: [PATCH 015/107] feat: add API endpoint to add languages --- backend/api.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/backend/api.py b/backend/api.py index 9c14ab60..2040c086 100644 --- a/backend/api.py +++ b/backend/api.py @@ -629,6 +629,31 @@ def returnAllLanguages(): return jsonify(languages), 200 +@api.route("/api/languages", methods=["POST"]) +def add_languages(): + data = request.get_json() + try: + get_all_existing_languages = models.Lang.query.all() + languages = {t.id: t for t in get_all_existing_languages} + for lang in data: + if lang["id"] not in languages: + lang_obj = models.Lang( + id=lang["id"], + label=lang["label"], + is_published=lang["is_published"], + is_default=lang["is_default"], + ) + db.session.add(lang_obj) + + db.session.commit() + + except Exception as exception: + db.session.rollback() + return jsonify({"error": str(exception)}), 400 + + return jsonify("languages added") + + @api.route("/api/logout", methods=["GET"]) def logout(): resp = Response("", 200) From 163f9bb59a16ad0482b97547e9ac0ffa1ecfb245 Mon Sep 17 00:00:00 2001 From: jules-jean-louis1 Date: Fri, 25 Oct 2024 08:04:48 +0200 Subject: [PATCH 016/107] feat: add API endpoints for updating and deleting languages --- backend/api.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/backend/api.py b/backend/api.py index 2040c086..0e115042 100644 --- a/backend/api.py +++ b/backend/api.py @@ -648,12 +648,37 @@ def add_languages(): db.session.commit() except Exception as exception: - db.session.rollback() + db.session.rollback() return jsonify({"error": str(exception)}), 400 return jsonify("languages added") +@api.route("/api/language/", methods=["PATCH"]) +def update_language(id): + data = request.get_json() + try: + models.Lang.query.filter_by(id=id).update(data) + db.session.commit() + except Exception as exception: + db.session.rollback() + return jsonify({"error": str(exception)}), 400 + + return jsonify("language updated"), 200 + + +@api.route("/api/language/", methods=["DELETE"]) +def delete_language(id): + try: + models.Lang.query.filter_by(id=id).delete() + db.session.commit() + except Exception as exception: + db.session.rollback() + return jsonify({"error": str(exception)}), 400 + + return jsonify("language deleted"), 200 + + @api.route("/api/logout", methods=["GET"]) def logout(): resp = Response("", 200) From 482cfd553c4bd84de0a622d560eff4947a15bc86 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Fri, 25 Oct 2024 10:10:49 +0200 Subject: [PATCH 017/107] feat: Add authentication check to language-related API routes --- backend/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/api.py b/backend/api.py index 0e115042..4a49281b 100644 --- a/backend/api.py +++ b/backend/api.py @@ -630,6 +630,7 @@ def returnAllLanguages(): @api.route("/api/languages", methods=["POST"]) +@fnauth.check_auth(6) def add_languages(): data = request.get_json() try: @@ -655,6 +656,7 @@ def add_languages(): @api.route("/api/language/", methods=["PATCH"]) +@fnauth.check_auth(2) def update_language(id): data = request.get_json() try: @@ -668,6 +670,7 @@ def update_language(id): @api.route("/api/language/", methods=["DELETE"]) +@fnauth.check_auth(6) def delete_language(id): try: models.Lang.query.filter_by(id=id).delete() From b1696bc80542bebc38b0384ac849e80207327026 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Tue, 22 Oct 2024 20:00:21 +0200 Subject: [PATCH 018/107] feat: wip multi langs --- backend/app.py | 12 ++++++++---- backend/routes.py | 45 +++++++++++++++++++++++++++++++++++---------- backend/utils.py | 15 ++++++++++----- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/backend/app.py b/backend/app.py index caaff381..ff3def93 100755 --- a/backend/app.py +++ b/backend/app.py @@ -3,7 +3,7 @@ from pypnusershub.login_manager import login_manager from routes import main as main_blueprint from flask import Flask -from flask_babel import Babel, get_locale +from flask_babel import Babel from flask_cors import CORS from api import api import config @@ -56,6 +56,10 @@ def __call__(self, environ, start_response): # app.wsgi_app = ReverseProxied(app.wsgi_app) CORS(app, supports_credentials=True) +@babel.localeselector +def determine_locale(): + return utils.getLocale() + app.register_blueprint(main_blueprint) app.register_blueprint(api) app.register_blueprint(custom_app.custom) @@ -71,9 +75,9 @@ def __call__(self, environ, start_response): def inject_to_tpl(): custom = custom_app.custom_inject_to_tpl() data = dict( - dbconf=utils.getDbConf(), - debug=app.debug, - locale=get_locale(), + dbconf=utils.getDbConf(), + debug=app.debug, + locale=utils.getLocale(), isMultiObservatories=utils.isMultiObservatories, getThumborUrl=utils.getThumborUrl, getCustomTpl=utils.getCustomTpl, diff --git a/backend/routes.py b/backend/routes.py index 19b7b6db..90f9767f 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -1,4 +1,5 @@ -from flask import render_template, Blueprint, abort +from flask import render_template, Blueprint, abort, request, redirect, url_for +from functools import wraps from sqlalchemy import text from sqlalchemy.sql.expression import desc import models @@ -21,12 +22,31 @@ themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) communes_schema = models.CommunesSchema(many=True) - -@main.route("/") -def home(): - sql = text( - "SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6" - ) +def localeGuard(f): + @wraps(f) + def decorated_function(*args, **kwargs): + locale = utils.getLocale() + if not utils.isMultiLangs() and locale is not None: + return redirect(url_for(request.endpoint)) + if utils.isMultiLangs() and locale is None: + matched_locale = request.accept_languages.best_match(['fr', 'en']) + if matched_locale is None: + return redirect('/') + return redirect(url_for(request.endpoint, locale=matched_locale)) + return f(*args, **kwargs) + return decorated_function + +def homeLocaleSelector(): + return "Select a language" + +@main.route('/') +@main.route('//') +def home(locale=None): + if utils.isMultiLangs() and locale is None: + return homeLocaleSelector() + if not utils.isMultiLangs() and locale is not None: + return redirect('/') + sql = text("SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6") sites_proxy = db.engine.execute(sql).fetchall() sites = [dict(row.items()) for row in sites_proxy] @@ -220,9 +240,12 @@ def site_photos_last(id_site): return render_template("site_photo.jinja", site=site, photo=photo) -@main.route("/sites") -def sites(): +@main.route('/sites') +@main.route('//sites/') +@localeGuard +def sites(locale=None): data = utils.getFiltersData() + print(locale) return render_template( "sites.jinja", @@ -232,7 +255,9 @@ def sites(): ) -@main.route("/legal-notices") +@main.route('/legal-notices/') +@main.route('//legal-notices/') +@localeGuard def legal_notices(): tpl = utils.getCustomTpl("legal_notices") diff --git a/backend/utils.py b/backend/utils.py index 0890cb8f..693ea79d 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -1,10 +1,10 @@ from base64 import urlsafe_b64encode -from flask import url_for +from flask import url_for, request import os from flask_sqlalchemy import SQLAlchemy from sqlalchemy import text import json -from flask_babel import get_locale, gettext +from flask_babel import gettext import random import string import models @@ -21,11 +21,16 @@ site_schema = models.TSiteSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) +def getLocale(): + return request.view_args.get('locale', None) + +def isMultiLangs(): + return True def getCustomTpl(name): - tpl_local = f"custom/{name}_{get_locale().__str__()}.jinja" - tpl_common = f"custom/{name}.jinja" - if os.path.exists(f"tpl/{tpl_local}"): + tpl_local = f'custom/{name}_{getLocale()}.jinja' + tpl_common = f'custom/{name}.jinja' + if os.path.exists(f'tpl/{tpl_local}'): return tpl_local if os.path.exists(f"tpl/{tpl_common}"): return tpl_common From 4b9a56560bbe07f77b1e3f103eb156922808a78f Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Wed, 23 Oct 2024 11:26:43 +0200 Subject: [PATCH 019/107] style: rm unused import --- backend/routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/routes.py b/backend/routes.py index 90f9767f..2d14f4cc 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -6,7 +6,6 @@ import utils from config import COMPARATOR_VERSION from datetime import datetime -from flask_babel import format_datetime import math import os From ff1edf871ad7639a7964af28b3d2d7c755add86e Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Wed, 23 Oct 2024 11:47:40 +0200 Subject: [PATCH 020/107] fix: date format from locale --- backend/routes.py | 2 +- backend/static/js/comparator-v2.js | 6 +++--- backend/tpl/components/comparator_v2.jinja | 1 + backend/utils.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/routes.py b/backend/routes.py index 2d14f4cc..315779b9 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -24,7 +24,7 @@ def localeGuard(f): @wraps(f) def decorated_function(*args, **kwargs): - locale = utils.getLocale() + locale = request.view_args.get('locale') if not utils.isMultiLangs() and locale is not None: return redirect(url_for(request.endpoint)) if utils.isMultiLangs() and locale is None: diff --git a/backend/static/js/comparator-v2.js b/backend/static/js/comparator-v2.js index 1e9edd0e..47c5bb39 100644 --- a/backend/static/js/comparator-v2.js +++ b/backend/static/js/comparator-v2.js @@ -31,17 +31,17 @@ geopsg.comparator = (options) => { const getFormatedDate = (value) => { if (options.dbconf.comparator_date_format == 'year') { - return value.toLocaleString('fr-FR', { + return value.toLocaleString(options.locale, { year: 'numeric', }); } if (options.dbconf.comparator_date_format == 'month') { - return value.toLocaleString('fr-FR', { + return value.toLocaleString(options.locale, { month: '2-digit', year: 'numeric', }); } else { - return value.toLocaleString('fr-FR', { + return value.toLocaleString(options.locale, { month: '2-digit', day: '2-digit', year: 'numeric', diff --git a/backend/tpl/components/comparator_v2.jinja b/backend/tpl/components/comparator_v2.jinja index 06416b4a..80ae9245 100644 --- a/backend/tpl/components/comparator_v2.jinja +++ b/backend/tpl/components/comparator_v2.jinja @@ -177,6 +177,7 @@ dbconf: {{dbconf | tojson}}, site: {{site | tojson}}, photos: {{photos | tojson}}, + locale: "{{locale}}", translations: { 'mode_sidebyside': "{{ _('comparatorv2.mode.sidebyside') }}", 'mode_split': "{{ _('comparatorv2.mode.split') }}", diff --git a/backend/utils.py b/backend/utils.py index 693ea79d..45f75132 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -22,10 +22,10 @@ themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) def getLocale(): - return request.view_args.get('locale', None) + return request.view_args.get('locale', 'fr') def isMultiLangs(): - return True + return False def getCustomTpl(name): tpl_local = f'custom/{name}_{getLocale()}.jinja' From fcc795e70b6f243e01597f070acd70664e40c6e6 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Wed, 23 Oct 2024 15:49:23 +0200 Subject: [PATCH 021/107] chore: mv hard coded string to i18n files --- backend/static/js/comparator-v2.js | 20 +- backend/static/js/sites.js | 4 +- backend/tpl/components/comparator_v2.jinja | 2 +- backend/tpl/components/sites-filter.jinja | 2 +- backend/tpl/home_multi_obs.jinja | 2 +- backend/tpl/site.jinja | 2 +- backend/tpl/sites.jinja | 18 +- .../i18n/en/LC_MESSAGES/messages.mo | Bin 0 -> 3623 bytes .../i18n/en/LC_MESSAGES/messages.po | 267 ++++++++++++++++++ .../i18n/fr/LC_MESSAGES/messages.mo | Bin 3269 -> 3752 bytes .../i18n/fr/LC_MESSAGES/messages.po | 159 +++++++---- docker/custom.sample/i18n/messages.pot | 146 ++++++---- 12 files changed, 484 insertions(+), 138 deletions(-) create mode 100644 docker/custom.sample/i18n/en/LC_MESSAGES/messages.mo create mode 100755 docker/custom.sample/i18n/en/LC_MESSAGES/messages.po mode change 100644 => 100755 docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po diff --git a/backend/static/js/comparator-v2.js b/backend/static/js/comparator-v2.js index 47c5bb39..8b013e82 100644 --- a/backend/static/js/comparator-v2.js +++ b/backend/static/js/comparator-v2.js @@ -76,7 +76,7 @@ geopsg.comparator = (options) => { comparedPhotos: [defaultItems[0], defaultItems[1]], thumbProps: thumbProps, thumbsListH: thumbProps.height + 30, - hiddenSelectors: [hideSelectorsOnInit, hideSelectorsOnInit] + hiddenSelectors: [hideSelectorsOnInit, hideSelectorsOnInit], }; }, mounted() { @@ -145,7 +145,7 @@ geopsg.comparator = (options) => { } classNames.push('selected'); - return classNames.join(' ') + return classNames.join(' '); }, updateLayers() { this.$bvModal.show('comparatorLoading'); @@ -230,41 +230,41 @@ geopsg.comparator = (options) => { if (options.dbconf.comparator_date_format == 'year') { sharedData.steps = [ { - label: '1 an', + label: options.translations.date_steps_1y, value: 3600 * 24 * 365, }, ]; } else if (options.dbconf.comparator_date_format == 'month') { sharedData.steps = [ { - label: '1 mois', + label: options.translations.date_steps_1m, value: 3600 * 24 * 30, }, { - label: '1 an', + label: options.translations.date_steps_1y, value: 3600 * 24 * 365, }, ]; } else { sharedData.steps = [ { - label: '0', + label: options.translations.date_steps_none, value: 0, }, { - label: '1 jour', + label: options.translations.date_steps_1d, value: 3600 * 24, }, { - label: '1 semaine', + label: options.translations.date_steps_1w, value: 3600 * 24 * 7, }, { - label: '1 mois', + label: options.translations.date_steps_1m, value: 3600 * 24 * 30, }, { - label: '1 an', + label: options.translations.date_steps_1y, value: 3600 * 24 * 365, }, ]; diff --git a/backend/static/js/sites.js b/backend/static/js/sites.js index bbeb9d87..59a34293 100644 --- a/backend/static/js/sites.js +++ b/backend/static/js/sites.js @@ -434,8 +434,8 @@ geopsg.initSites = (options) => { try { await navigator.clipboard.writeText(this.shareUrl); - this.$bvToast.toast('Le lien est prêt à être coller.', { - title: 'Copié !', + this.$bvToast.toast(options.translations.share_copy_success_message, { + title: options.translations.share_copy_success_title, variant: 'success', solid: true, }); diff --git a/backend/tpl/components/comparator_v2.jinja b/backend/tpl/components/comparator_v2.jinja index 80ae9245..384c46cb 100644 --- a/backend/tpl/components/comparator_v2.jinja +++ b/backend/tpl/components/comparator_v2.jinja @@ -160,7 +160,7 @@ > {% if dbconf.comparator_date_step_button == 'False': %} {% else %} - + diff --git a/backend/tpl/components/sites-filter.jinja b/backend/tpl/components/sites-filter.jinja index 81781080..5a16050c 100644 --- a/backend/tpl/components/sites-filter.jinja +++ b/backend/tpl/components/sites-filter.jinja @@ -50,7 +50,7 @@ @click="onCancelClick()" class="btn btn-outline-secondary"> - Réinitialiser + {{_('sites.filters.reset')}} \ No newline at end of file diff --git a/backend/tpl/home_multi_obs.jinja b/backend/tpl/home_multi_obs.jinja index 0ee4dcdc..7244aae0 100644 --- a/backend/tpl/home_multi_obs.jinja +++ b/backend/tpl/home_multi_obs.jinja @@ -45,7 +45,7 @@ {% endfor %} diff --git a/backend/tpl/site.jinja b/backend/tpl/site.jinja index b9ba2de0..2a44198f 100644 --- a/backend/tpl/site.jinja +++ b/backend/tpl/site.jinja @@ -82,7 +82,7 @@
-
Mots clés
+
{{ _('obs_point.keywords') }}
{% for un_sous_theme in site.stheme %} diff --git a/backend/tpl/sites.jinja b/backend/tpl/sites.jinja index 2939d2bd..b93fe14d 100644 --- a/backend/tpl/sites.jinja +++ b/backend/tpl/sites.jinja @@ -74,7 +74,9 @@
- + + {{_('sites.observation_points.item')}} +
@@ -107,10 +109,10 @@
- Partager + {{_('map.share.button')}}
- Partager + {{_('map.share.button')}}
@@ -134,7 +136,7 @@ class="btn d-flex justify-content-between p-3 btn-toggle" v-b-toggle="'app_map_legend_collapse'" > - Légende + {{_('map.legend.title')}} @@ -144,8 +146,8 @@
- -

Copier et partager le lien ci-dessus

+ +

{{_('map.share.dialog.message')}}

f34}ko$cvr zx@vYadJ@b*4-pRmLBt#)K@WoB!Kg?S#FJpW#6#jG>LrMXAE$w}ZREL2xH{0;ITIknEoW2f&xXE#P^>OCZH}*{rXE&@ z(!RGqiuXgK{~DxtzXz$Fzk}4jZJQXo6U>6=(Ccv!EoEP!2q+!93T_6kfoKu?(a3Ls z2$B5)B6aqcAvvQfvk@fyCc`b@U0Ck|4}gz@)Td`bY-RIiy#UfVFM^csOCUmJuN(bE z!}me5y8@Ek7a-+x&F~M9>a+ug;*;$IN5C;~5R@S8e+@*+>ml-Es=>UhiOHz66?cMrG++zC=&#zFF5G<*i6cxxcpp8?^5y#P{w&Vdxyn;`AG z2-5lQ8uD-^qdL5ca@*UJfS2iOjcC?HavbN`FVb}aXi`W*S zx3*GgC)R7LwXXxwN3FDo)(|T@)9*am0abJDU5jh0+H0yp8Lo&gkdEhv_2oO7(i67w zJyC7b-+sy{@HMMRNhNb-)Qw9y&^BaKtf5lA7HX9)4a7LLn?gG#wvH$@Xb8tcLZP!@Cr9+GQ@P( zTo5tqBdf@&67d3EFCWqLSco%GAY)Nkf^dK zoygo>l`0ZRAnLfuwygGqL6%0Y8={H8dL_x-C4p)#8GF4>aOJe$lp!)yj9RXUhpCS1ws7L~{EkT3VYLzq)e7`7Q)NRaIE`r%E|#Z-tF0-|IwfKSPdPqUoIR1BDDzYKVrgo2W}qk{8Eb1=)qT%8 z+Nvw7EOR_RePpU|_{ijBF<&YT%*~dq2{ewHU`=9B=XiEFyWh$TTiFqw$>v6~dx!A@ zL#rqjd^%~#E;DLnvQ{?3Gnw4T{=LH)*cBY5t#a&yDxm+*96xrlP~bC1rt|#ZvZKdg z8rEC5;W=K5mljr8I8Bo9gN4H3z_(>wA0LtAjdhUw?#}CK2i+@G5=}=48tkJF=1z!`fl@DD> z(>upUtG*sME<%APO`w+T96#z*MZmLm<^Yahc|2>S7PZI1dz_$LrH>K^&R+f8gIy-LP7DkFHZ#i`Y_fFMSjhaB- ztkkMn8cDdUocv9MPt&zc3zXw~2E*zQWll4Yic?Gm-%5AMH1EtjPJ@hWS6i)?{0nK1 B14;k@ literal 0 HcmV?d00001 diff --git a/docker/custom.sample/i18n/en/LC_MESSAGES/messages.po b/docker/custom.sample/i18n/en/LC_MESSAGES/messages.po new file mode 100755 index 00000000..d709570f --- /dev/null +++ b/docker/custom.sample/i18n/en/LC_MESSAGES/messages.po @@ -0,0 +1,267 @@ +# English translations for PROJECT. +# Copyright (C) 2018 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2024-10-23 12:32+0000\n" +"PO-Revision-Date: 2018-12-21 11:34+0100\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.16.0\n" + +#: utils.py:127 +msgid "sites.filter.themes" +msgstr "Topic" + +#: utils.py:131 +msgid "sites.filter.subthemes" +msgstr "Sub-topic" + +#: utils.py:136 +msgid "sites.filter.township" +msgstr "City" + +#: utils.py:141 +msgid "sites.filter.years" +msgstr "Year" + +#: utils.py:260 +msgid "sites.filter.obervatories" +msgstr "City" + +#: tpl/gallery.jinja:3 +msgid "gallery.meta_title" +msgstr "Photo Gallery" + +#: tpl/gallery.jinja:18 +msgid "gallery.title" +msgstr "Photo Gallery" + +#: tpl/components/sites-filter.jinja:8 tpl/components/sites-filter.jinja:15 +#: tpl/gallery.jinja:76 tpl/sites.jinja:52 +msgid "sites.filters.nb_result" +msgstr "Filter: %(nb)s result(s)" + +#: tpl/home_mono_obs.jinja:3 tpl/home_multi_obs.jinja:3 +msgid "home.meta_title" +msgstr "GeoPaysages: Photographic Observatory of Landscapes" + +#: tpl/home_mono_obs.jinja:17 tpl/home_multi_obs.jinja:19 +msgid "home.title_mobile" +msgstr "Home" + +#: tpl/components/home-carousel.jinja:3 tpl/components/home-carousel.jinja:24 +#: tpl/home_mono_obs.jinja:22 tpl/home_multi_obs.jinja:24 +msgid "home.title" +msgstr "Nature-Based Solutions Education Network" + +#: tpl/home_mono_obs.jinja:36 tpl/home_multi_obs.jinja:61 +msgid "home.block.explore_sites" +msgstr "Explore Observation Sites" + +#: tpl/home_mono_obs.jinja:43 +msgid "home.block.discover" +msgstr "Discover this Observation Site" + +#: tpl/home_mono_obs.jinja:46 +msgid "home.block.photography" +msgstr "Photography" + +#: tpl/home_multi_obs.jinja:48 +msgid "home.block.discover_observatories" +msgstr "Discover this Observation Site" + +#: tpl/layout.jinja:72 +msgid "footer.internal_title" +msgstr "The Site" + +#: tpl/layout.jinja:76 +msgid "footer.internal_links.home" +msgstr "Home" + +#: tpl/layout.jinja:81 +msgid "footer.internal_links.sites" +msgstr "Observation Sites" + +#: tpl/layout.jinja:86 +msgid "footer.internal_links.gallery" +msgstr "Photo Gallery" + +#: tpl/layout.jinja:92 +msgid "footer.internal_links.contact" +msgstr "Contact Us" + +#: tpl/layout.jinja:101 +msgid "footer.external_title" +msgstr "Our Territory" + +#: tpl/site.jinja:31 +msgid "site.title_mobile" +msgstr "Observation Site" + +#: tpl/site.jinja:58 +msgid "obs_point.buttons.notice" +msgstr "Technical Notice for the Photographer" + +#: tpl/site.jinja:63 +msgid "obs_point.buttons.obs" +msgstr "Make an Observation" + +#: tpl/site.jinja:68 +msgid "obs_point.description" +msgstr "Site Presentation" + +#: tpl/site.jinja:75 tpl/site.jinja:94 tpl/site.jinja:111 +msgid "obs_point.buttons.read_more" +msgstr "Read More" + +#: tpl/site.jinja:76 tpl/site.jinja:95 tpl/site.jinja:112 +msgid "obs_point.buttons.read_less" +msgstr "Read Less" + +#: tpl/site.jinja:85 +msgid "obs_point.keywords" +msgstr "Keywords" + +#: tpl/site.jinja:104 +msgid "obs_point.testimonials" +msgstr "Testimonials" + +#: tpl/sites.jinja:4 +msgid "sites.meta_title" +msgstr "Observation Sites" + +#: tpl/sites.jinja:29 +msgid "sites.title" +msgstr "Observation Sites" + +#: tpl/sites.jinja:51 +msgid "sites.observation_points.title" +msgstr "Observation Sites" + +#: tpl/sites.jinja:77 +msgid "sites.observation_points.item" +msgstr "observation site(s)" + +#: tpl/sites.jinja:110 tpl/sites.jinja:113 +msgid "map.share.button" +msgstr "Share" + +#: tpl/sites.jinja:119 +msgid "map.legend.obervatories" +msgstr "city(ies)" + +#: tpl/sites.jinja:125 +msgid "map.legend.themes" +msgstr "Theme(s)" + +#: tpl/sites.jinja:137 +msgid "map.legend.title" +msgstr "Legend" + +#: tpl/sites.jinja:147 +msgid "map.share.dialog.title" +msgstr "Share" + +#: tpl/sites.jinja:148 +msgid "map.share.dialog.message" +msgstr "Copy and share the link above" + +#: tpl/sites.jinja:165 +msgid "map.share.copy_success.title" +msgstr "Copied!" + +#: tpl/sites.jinja:166 +msgid "map.share.copy.copy_success.message" +msgstr "The link is ready to paste." + +#: tpl/components/comparator_v1.jinja:8 +msgid "obs_point.buttons.download" +msgstr "Download" + +#: tpl/components/comparator_v2.jinja:78 +msgid "comparatorv2.loading" +msgstr "Loading" + +#: tpl/components/comparator_v2.jinja:120 +msgid "comparatorv2.date.filter.title" +msgstr "Filter by Date" + +#: tpl/components/comparator_v2.jinja:122 +msgid "comparatorv2.date.filter.start" +msgstr "Start" + +#: tpl/components/comparator_v2.jinja:131 +msgid "comparatorv2.date.filter.end" +msgstr "End" + +#: tpl/components/comparator_v2.jinja:163 +msgid "comparatorv2.date.step" +msgstr "Step" + +#: tpl/components/comparator_v2.jinja:182 +msgid "comparatorv2.mode.sidebyside" +msgstr "Overlay" + +#: tpl/components/comparator_v2.jinja:183 +msgid "comparatorv2.mode.split" +msgstr "Side by Side" + +#: tpl/components/comparator_v2.jinja:184 +msgid "comparatorv2.date.steps.none" +msgstr "0" + +#: tpl/components/comparator_v2.jinja:185 +msgid "comparatorv2.date.steps.1d" +msgstr "1 day" + +#: tpl/components/comparator_v2.jinja:186 +msgid "comparatorv2.date.steps.1w" +msgstr "1 week" + +#: tpl/components/comparator_v2.jinja:187 +msgid "comparatorv2.date.steps.1m" +msgstr "1 month" + +#: tpl/components/comparator_v2.jinja:188 +msgid "comparatorv2.date.steps.1y" +msgstr "1 year" + +#: tpl/components/legal-footer.jinja:1 tpl/custom/footer.jinja:3 +msgid "footer.copyright" +msgstr "© 2023 - GeoLandscapes, free and open-source software" + +#: tpl/components/sites-filter.jinja:22 +msgid "map.filters.title" +msgstr "Filter by" + +#: tpl/components/sites-filter.jinja:53 +msgid "sites.filters.reset" +msgstr "Reset" + +#: tpl/custom/main_menu.jinja:3 +msgid "header.nav.home" +msgstr "Home" + +#: tpl/custom/main_menu.jinja:8 +msgid "header.nav.sites" +msgstr "Observation Sites" + +#~ msgid "home.map_block_title" +#~ msgstr "Map of cities" + +#~ msgid "header.nav.about" +#~ msgstr "About" + +#~ msgid "header.nav.gallery" +#~ msgstr "Photo Gallery" + diff --git a/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.mo b/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.mo index 2e74c3087fe66028626248c6ae84f55e0e1bea74..1f9529057489252cd4fece1ede0ea9b3da2e2cc2 100644 GIT binary patch delta 1534 zcmZ9~Piz!b9Ki9{Y_~1jWf58|#Uj&+p#-|pKLSz11%r_wA~7U*u$j(2m%-`IX5JJ- zJ**cFqN#tLz>!8{WQj(T8bmSCWMc>i5~>G#06icFV|oG&K@fB)a#xmRZbS#|KH@b#^x^bwBYAT#L^-k2sGzr%~p; zh@Cj+;(6>+s-|*TrS7Al4_D(buEVFDWt0Vu;{!O2((h|rh2P_HyooFD7Rr2?7Nt6{ z6ML~A*WezM!UJ5v`f8Gw9ElUS5>GlmLMl{Wx_B04;v3G}_z-cnb#aHAP!`yYvcM=x z;YVHm9hAb)p={(^k@eLz*Kh}AM}@Y<33^cq+JW7;$K^{XJ9`yn!naVyeTY)<1=oHR zrQlysPV{$_E9_XNR67n~ZHoKe$qU!2u5b)&O3k7SoI}a~>hkl>+sF|0CvuG{%WU$u z$ss9~N4b(NEa1a9f+HyNhAuzZ&iPBn*GaH>bsA-7XIwmk(tgo-+2wyk>Gv~wcnjrE z)|p?72a&Hjz+)SZVn3cjS?3#Mh`N;L{N+#c0}0vD4dj&6yz>w2BmUdPeN?l7cr!}B z{U`+NgqkdruwvhWcXzvg_eU|-0jc0RLqW;S!G$-a?&)P9_8D~!ftMkh!8G)_Wo?C06- z@3sWBalcxObXiwQ#WYMKtt4R%`-v`&O{8gDS&|6i>an6cM$JSJXk&`wJpA&~LH}v~ z|BR*3kK*#uQ5|kA*Rn9cjK7KZq2FPVMRUJsIvuLqcqWpSM?LGCpoT#x-US74tle8ym~zY^cWpn*uWpsL LezVC=<+uL>0oWVs delta 1078 zcmYMyOGs2v7{Ku(<7>tltMOf_6HFYt6@Pin-(GT zIz)*@6a-oHQi8yC5+p$sZNx=8gM?hTs}}YDm=j&*-rxPsJ?Hz*V;R_xSjZo@r!Z8=+DoMi()zbdQE%`tBBLMRjG`c=B0v$ zyI70!#!n~(HgE^}3v&H-W0<%HL)ecbQiG;_9Jdf>P5X5$BYuF@_!=wmBbHNNt??oQ zeqjIug-UJ13S$GxjJr&H7-eE3C#8w8g}6ejNoSs;wH)t{K5TL#j+c*A0@$gl%2@%kRzN%DKv{R!ABUy1(cm#K~kv= zlzu-;PUk-Os3rw#c*unIn7AA1s}7=U?Gcp1Cruo~dSVA<+!f}#%b;hbb)af6Ab^lT}l1#ZtH?7~7bd@Dt zC*eg#8{AmhPEA^#<0f=#a9>rQmGbP!sGW}d`{Ft&JFSO;*Yt^y7e2Z)H|{!P32V$w Rt3#4Re-91mY-uBR`CsMBlM?^{ diff --git a/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po b/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po old mode 100644 new mode 100755 index 22e4e651..1ad0b7e5 --- a/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po +++ b/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-09-14 10:23+0000\n" +"POT-Creation-Date: 2024-10-23 12:32+0000\n" "PO-Revision-Date: 2018-12-21 11:34+0100\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -16,25 +16,25 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.16.0\n" -#: utils.py:215 +#: utils.py:127 msgid "sites.filter.themes" msgstr "Thème" -#: utils.py:219 +#: utils.py:131 msgid "sites.filter.subthemes" msgstr "Sous-thème" -#: utils.py:224 +#: utils.py:136 msgid "sites.filter.township" msgstr "Commune" -#: utils.py:229 +#: utils.py:141 msgid "sites.filter.years" msgstr "Année" -#: utils.py:348 +#: utils.py:260 msgid "sites.filter.obervatories" msgstr "Observatoire" @@ -42,12 +42,12 @@ msgstr "Observatoire" msgid "gallery.meta_title" msgstr "Galerie photo" -#: tpl/gallery.jinja:19 +#: tpl/gallery.jinja:18 msgid "gallery.title" msgstr "Galerie photo" #: tpl/components/sites-filter.jinja:8 tpl/components/sites-filter.jinja:15 -#: tpl/gallery.jinja:75 tpl/sites.jinja:52 +#: tpl/gallery.jinja:76 tpl/sites.jinja:52 msgid "sites.filters.nb_result" msgstr "Filtre(s) : %(nb)s résultat(s)" @@ -55,56 +55,56 @@ msgstr "Filtre(s) : %(nb)s résultat(s)" msgid "home.meta_title" msgstr "GeoPaysages : Observatoire photographique des paysages" -#: tpl/components/home-carousel.jinja:2 tpl/components/home-carousel.jinja:20 -#: tpl/home_mono_obs.jinja:19 tpl/home_multi_obs.jinja:21 -msgid "home.title" -msgstr "Observatoire photographique des paysages" - -#: tpl/home_mono_obs.jinja:23 tpl/home_multi_obs.jinja:25 +#: tpl/home_mono_obs.jinja:17 tpl/home_multi_obs.jinja:19 msgid "home.title_mobile" msgstr "Accueil" -#: tpl/home_mono_obs.jinja:39 tpl/home_multi_obs.jinja:94 +#: tpl/components/home-carousel.jinja:3 tpl/components/home-carousel.jinja:24 +#: tpl/home_mono_obs.jinja:22 tpl/home_multi_obs.jinja:24 +msgid "home.title" +msgstr "Observatoire photographique des paysages" + +#: tpl/home_mono_obs.jinja:36 tpl/home_multi_obs.jinja:61 msgid "home.block.explore_sites" msgstr "Explorer les sites d'observation" -#: tpl/home_mono_obs.jinja:46 +#: tpl/home_mono_obs.jinja:43 msgid "home.block.discover" msgstr "Découvrir ce site d'observation" -#: tpl/home_mono_obs.jinja:49 +#: tpl/home_mono_obs.jinja:46 msgid "home.block.photography" msgstr "Photographie" -#: tpl/home_multi_obs.jinja:90 -msgid "home.map_block_title" -msgstr "Carte des observatoires" +#: tpl/home_multi_obs.jinja:48 +msgid "home.block.discover_observatories" +msgstr "Découvrez nos observatoires" -#: tpl/layout.jinja:75 +#: tpl/layout.jinja:72 msgid "footer.internal_title" msgstr "Le site" -#: tpl/layout.jinja:79 +#: tpl/layout.jinja:76 msgid "footer.internal_links.home" msgstr "Accueil" -#: tpl/layout.jinja:84 +#: tpl/layout.jinja:81 msgid "footer.internal_links.sites" msgstr "Sites d'observation" -#: tpl/layout.jinja:89 +#: tpl/layout.jinja:86 msgid "footer.internal_links.gallery" msgstr "Galerie photo" -#: tpl/layout.jinja:95 +#: tpl/layout.jinja:92 msgid "footer.internal_links.contact" msgstr "Contactez-nous" -#: tpl/layout.jinja:104 +#: tpl/layout.jinja:101 msgid "footer.external_title" msgstr "Notre territoire" -#: tpl/site.jinja:41 +#: tpl/site.jinja:31 msgid "site.title_mobile" msgstr "site d'observation" @@ -120,15 +120,19 @@ msgstr "Faire une observation" msgid "obs_point.description" msgstr "Présentation du site" -#: tpl/site.jinja:75 tpl/site.jinja:93 tpl/site.jinja:106 +#: tpl/site.jinja:75 tpl/site.jinja:94 tpl/site.jinja:111 msgid "obs_point.buttons.read_more" msgstr "Lire plus" -#: tpl/site.jinja:76 tpl/site.jinja:94 tpl/site.jinja:107 +#: tpl/site.jinja:76 tpl/site.jinja:95 tpl/site.jinja:112 msgid "obs_point.buttons.read_less" msgstr "Lire moins" -#: tpl/site.jinja:99 +#: tpl/site.jinja:85 +msgid "obs_point.keywords" +msgstr "Mots clés" + +#: tpl/site.jinja:104 msgid "obs_point.testimonials" msgstr "Témoignages" @@ -144,86 +148,114 @@ msgstr "Sites d'observation" msgid "sites.observation_points.title" msgstr "Sites d'observation" -#: tpl/sites.jinja:117 +#: tpl/sites.jinja:77 +msgid "sites.observation_points.item" +msgstr "site(s) d'observation" + +#: tpl/sites.jinja:110 tpl/sites.jinja:113 +msgid "map.share.button" +msgstr "Partager" + +#: tpl/sites.jinja:119 msgid "map.legend.obervatories" msgstr "Observatoire(s)" -#: tpl/sites.jinja:123 +#: tpl/sites.jinja:125 msgid "map.legend.themes" msgstr "Thème(s)" +#: tpl/sites.jinja:137 +msgid "map.legend.title" +msgstr "Légende" + +#: tpl/sites.jinja:147 +msgid "map.share.dialog.title" +msgstr "Partager" + +#: tpl/sites.jinja:148 +msgid "map.share.dialog.message" +msgstr "Copier et partager le lien ci-dessus" + +#: tpl/sites.jinja:165 +msgid "map.share.copy_success.title" +msgstr "Copié !" + +#: tpl/sites.jinja:166 +msgid "map.share.copy.copy_success.message" +msgstr "Le lien est prêt à être coller." + #: tpl/components/comparator_v1.jinja:8 msgid "obs_point.buttons.download" msgstr "Télécharger" -#: tpl/components/comparator_v2.jinja:57 +#: tpl/components/comparator_v2.jinja:78 msgid "comparatorv2.loading" msgstr "Chargement" -#: tpl/components/comparator_v2.jinja:95 +#: tpl/components/comparator_v2.jinja:120 msgid "comparatorv2.date.filter.title" msgstr "Filtrer par date" -#: tpl/components/comparator_v2.jinja:97 +#: tpl/components/comparator_v2.jinja:122 msgid "comparatorv2.date.filter.start" msgstr "Début" -#: tpl/components/comparator_v2.jinja:106 +#: tpl/components/comparator_v2.jinja:131 msgid "comparatorv2.date.filter.end" msgstr "Fin" -#: tpl/components/comparator_v2.jinja:156 +#: tpl/components/comparator_v2.jinja:163 +msgid "comparatorv2.date.step" +msgstr "Pas" + +#: tpl/components/comparator_v2.jinja:182 msgid "comparatorv2.mode.sidebyside" msgstr "Superposition" -#: tpl/components/comparator_v2.jinja:157 +#: tpl/components/comparator_v2.jinja:183 msgid "comparatorv2.mode.split" msgstr "Côte à côte" -#: tpl/components/comparator_v2.jinja:158 +#: tpl/components/comparator_v2.jinja:184 msgid "comparatorv2.date.steps.none" msgstr "0" -#: tpl/components/comparator_v2.jinja:159 +#: tpl/components/comparator_v2.jinja:185 msgid "comparatorv2.date.steps.1d" msgstr "1 jour" -#: tpl/components/comparator_v2.jinja:160 +#: tpl/components/comparator_v2.jinja:186 msgid "comparatorv2.date.steps.1w" msgstr "1 semaine" -#: tpl/components/comparator_v2.jinja:161 +#: tpl/components/comparator_v2.jinja:187 msgid "comparatorv2.date.steps.1m" msgstr "1 mois" -#: tpl/components/comparator_v2.jinja:162 +#: tpl/components/comparator_v2.jinja:188 msgid "comparatorv2.date.steps.1y" msgstr "1 an" -#: tpl/components/legal-footer.jinja:1 +#: tpl/components/legal-footer.jinja:1 tpl/custom/footer.jinja:3 msgid "footer.copyright" msgstr "© 2023 - GeoPaysages, logiciel libre et open-source" -#: tpl/components/main-nav.jinja:3 +#: tpl/components/sites-filter.jinja:22 +msgid "map.filters.title" +msgstr "Filtrer par" + +#: tpl/components/sites-filter.jinja:53 +msgid "sites.filters.reset" +msgstr "Réinitialiser" + +#: tpl/custom/main_menu.jinja:3 msgid "header.nav.home" msgstr "Accueil" -#: tpl/components/main-nav.jinja:7 -msgid "header.nav.about" -msgstr "À propos" - -#: tpl/components/main-nav.jinja:11 +#: tpl/custom/main_menu.jinja:8 msgid "header.nav.sites" msgstr "Sites d'observation" -#: tpl/components/main-nav.jinja:14 -msgid "header.nav.gallery" -msgstr "Galerie photo" - -#: tpl/components/sites-filter.jinja:22 -msgid "map.filters.title" -msgstr "Filtrer par" - #~ msgid "map.filter.themes" #~ msgstr "Thème" @@ -269,3 +301,12 @@ msgstr "Filtrer par" #~ msgid "comparatorv2.mode.title" #~ msgstr "Disposition des photos" +#~ msgid "home.map_block_title" +#~ msgstr "Carte des observatoires" + +#~ msgid "header.nav.about" +#~ msgstr "À propos" + +#~ msgid "header.nav.gallery" +#~ msgstr "Galerie photo" + diff --git a/docker/custom.sample/i18n/messages.pot b/docker/custom.sample/i18n/messages.pot index 866f2112..52d31162 100644 --- a/docker/custom.sample/i18n/messages.pot +++ b/docker/custom.sample/i18n/messages.pot @@ -1,39 +1,39 @@ # Translations template for PROJECT. -# Copyright (C) 2022 ORGANIZATION +# Copyright (C) 2024 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2022. +# FIRST AUTHOR , 2024. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-09-14 10:23+0000\n" +"POT-Creation-Date: 2024-10-23 12:32+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.16.0\n" -#: utils.py:215 +#: utils.py:127 msgid "sites.filter.themes" msgstr "" -#: utils.py:219 +#: utils.py:131 msgid "sites.filter.subthemes" msgstr "" -#: utils.py:224 +#: utils.py:136 msgid "sites.filter.township" msgstr "" -#: utils.py:229 +#: utils.py:141 msgid "sites.filter.years" msgstr "" -#: utils.py:348 +#: utils.py:260 msgid "sites.filter.obervatories" msgstr "" @@ -41,12 +41,12 @@ msgstr "" msgid "gallery.meta_title" msgstr "" -#: tpl/gallery.jinja:19 +#: tpl/gallery.jinja:18 msgid "gallery.title" msgstr "" #: tpl/components/sites-filter.jinja:8 tpl/components/sites-filter.jinja:15 -#: tpl/gallery.jinja:75 tpl/sites.jinja:52 +#: tpl/gallery.jinja:76 tpl/sites.jinja:52 msgid "sites.filters.nb_result" msgstr "" @@ -54,56 +54,56 @@ msgstr "" msgid "home.meta_title" msgstr "" -#: tpl/components/home-carousel.jinja:2 tpl/components/home-carousel.jinja:20 -#: tpl/home_mono_obs.jinja:19 tpl/home_multi_obs.jinja:21 -msgid "home.title" +#: tpl/home_mono_obs.jinja:17 tpl/home_multi_obs.jinja:19 +msgid "home.title_mobile" msgstr "" -#: tpl/home_mono_obs.jinja:23 tpl/home_multi_obs.jinja:25 -msgid "home.title_mobile" +#: tpl/components/home-carousel.jinja:3 tpl/components/home-carousel.jinja:24 +#: tpl/home_mono_obs.jinja:22 tpl/home_multi_obs.jinja:24 +msgid "home.title" msgstr "" -#: tpl/home_mono_obs.jinja:39 tpl/home_multi_obs.jinja:94 +#: tpl/home_mono_obs.jinja:36 tpl/home_multi_obs.jinja:61 msgid "home.block.explore_sites" msgstr "" -#: tpl/home_mono_obs.jinja:46 +#: tpl/home_mono_obs.jinja:43 msgid "home.block.discover" msgstr "" -#: tpl/home_mono_obs.jinja:49 +#: tpl/home_mono_obs.jinja:46 msgid "home.block.photography" msgstr "" -#: tpl/home_multi_obs.jinja:90 -msgid "home.map_block_title" +#: tpl/home_multi_obs.jinja:48 +msgid "home.block.discover_observatories" msgstr "" -#: tpl/layout.jinja:75 +#: tpl/layout.jinja:72 msgid "footer.internal_title" msgstr "" -#: tpl/layout.jinja:79 +#: tpl/layout.jinja:76 msgid "footer.internal_links.home" msgstr "" -#: tpl/layout.jinja:84 +#: tpl/layout.jinja:81 msgid "footer.internal_links.sites" msgstr "" -#: tpl/layout.jinja:89 +#: tpl/layout.jinja:86 msgid "footer.internal_links.gallery" msgstr "" -#: tpl/layout.jinja:95 +#: tpl/layout.jinja:92 msgid "footer.internal_links.contact" msgstr "" -#: tpl/layout.jinja:104 +#: tpl/layout.jinja:101 msgid "footer.external_title" msgstr "" -#: tpl/site.jinja:41 +#: tpl/site.jinja:31 msgid "site.title_mobile" msgstr "" @@ -119,15 +119,19 @@ msgstr "" msgid "obs_point.description" msgstr "" -#: tpl/site.jinja:75 tpl/site.jinja:93 tpl/site.jinja:106 +#: tpl/site.jinja:75 tpl/site.jinja:94 tpl/site.jinja:111 msgid "obs_point.buttons.read_more" msgstr "" -#: tpl/site.jinja:76 tpl/site.jinja:94 tpl/site.jinja:107 +#: tpl/site.jinja:76 tpl/site.jinja:95 tpl/site.jinja:112 msgid "obs_point.buttons.read_less" msgstr "" -#: tpl/site.jinja:99 +#: tpl/site.jinja:85 +msgid "obs_point.keywords" +msgstr "" + +#: tpl/site.jinja:104 msgid "obs_point.testimonials" msgstr "" @@ -143,83 +147,111 @@ msgstr "" msgid "sites.observation_points.title" msgstr "" -#: tpl/sites.jinja:117 +#: tpl/sites.jinja:77 +msgid "sites.observation_points.item" +msgstr "" + +#: tpl/sites.jinja:110 tpl/sites.jinja:113 +msgid "map.share.button" +msgstr "" + +#: tpl/sites.jinja:119 msgid "map.legend.obervatories" msgstr "" -#: tpl/sites.jinja:123 +#: tpl/sites.jinja:125 msgid "map.legend.themes" msgstr "" +#: tpl/sites.jinja:137 +msgid "map.legend.title" +msgstr "" + +#: tpl/sites.jinja:147 +msgid "map.share.dialog.title" +msgstr "" + +#: tpl/sites.jinja:148 +msgid "map.share.dialog.message" +msgstr "" + +#: tpl/sites.jinja:165 +msgid "map.share.copy_success.title" +msgstr "" + +#: tpl/sites.jinja:166 +msgid "map.share.copy.copy_success.message" +msgstr "" + #: tpl/components/comparator_v1.jinja:8 msgid "obs_point.buttons.download" msgstr "" -#: tpl/components/comparator_v2.jinja:57 +#: tpl/components/comparator_v2.jinja:78 msgid "comparatorv2.loading" msgstr "" -#: tpl/components/comparator_v2.jinja:95 +#: tpl/components/comparator_v2.jinja:120 msgid "comparatorv2.date.filter.title" msgstr "" -#: tpl/components/comparator_v2.jinja:97 +#: tpl/components/comparator_v2.jinja:122 msgid "comparatorv2.date.filter.start" msgstr "" -#: tpl/components/comparator_v2.jinja:106 +#: tpl/components/comparator_v2.jinja:131 msgid "comparatorv2.date.filter.end" msgstr "" -#: tpl/components/comparator_v2.jinja:156 +#: tpl/components/comparator_v2.jinja:163 +msgid "comparatorv2.date.step" +msgstr "" + +#: tpl/components/comparator_v2.jinja:182 msgid "comparatorv2.mode.sidebyside" msgstr "" -#: tpl/components/comparator_v2.jinja:157 +#: tpl/components/comparator_v2.jinja:183 msgid "comparatorv2.mode.split" msgstr "" -#: tpl/components/comparator_v2.jinja:158 +#: tpl/components/comparator_v2.jinja:184 msgid "comparatorv2.date.steps.none" msgstr "" -#: tpl/components/comparator_v2.jinja:159 +#: tpl/components/comparator_v2.jinja:185 msgid "comparatorv2.date.steps.1d" msgstr "" -#: tpl/components/comparator_v2.jinja:160 +#: tpl/components/comparator_v2.jinja:186 msgid "comparatorv2.date.steps.1w" msgstr "" -#: tpl/components/comparator_v2.jinja:161 +#: tpl/components/comparator_v2.jinja:187 msgid "comparatorv2.date.steps.1m" msgstr "" -#: tpl/components/comparator_v2.jinja:162 +#: tpl/components/comparator_v2.jinja:188 msgid "comparatorv2.date.steps.1y" msgstr "" -#: tpl/components/legal-footer.jinja:1 +#: tpl/components/legal-footer.jinja:1 tpl/custom/footer.jinja:3 msgid "footer.copyright" msgstr "" -#: tpl/components/main-nav.jinja:3 -msgid "header.nav.home" -msgstr "" - -#: tpl/components/main-nav.jinja:7 -msgid "header.nav.about" +#: tpl/components/sites-filter.jinja:22 +msgid "map.filters.title" msgstr "" -#: tpl/components/main-nav.jinja:11 -msgid "header.nav.sites" +#: tpl/components/sites-filter.jinja:53 +msgid "sites.filters.reset" msgstr "" -#: tpl/components/main-nav.jinja:14 -msgid "header.nav.gallery" +#: tpl/custom/main_menu.jinja:3 +msgid "header.nav.home" msgstr "" -#: tpl/components/sites-filter.jinja:22 -msgid "map.filters.title" +#: tpl/custom/main_menu.jinja:8 +msgid "header.nav.sites" msgstr "" From 0e6e06fd7933037d43856f7458edcc719f2775a5 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 12:04:47 +0200 Subject: [PATCH 022/107] feat: wip add translation table --- backend/api.py | 9 +- .../versions/d7fd422e1054_translations.py | 223 ++++++++++++++++++ backend/models.py | 200 ++++++++++++---- 3 files changed, 384 insertions(+), 48 deletions(-) create mode 100644 backend/migrations/versions/d7fd422e1054_translations.py diff --git a/backend/api.py b/backend/api.py index b60f7946..82d84eb1 100644 --- a/backend/api.py +++ b/backend/api.py @@ -79,7 +79,12 @@ def returnDdConf(): @api.route("/api/observatories", methods=["GET"]) def returnAllObservatories(): - get_all = models.Observatory.query.order_by("title").all() + get_all = ( + models.Observatory.query + .join(models.ObservatoryTranslation) + .order_by(models.ObservatoryTranslation.title) + .all() + ) items = observatories_schema.dump(get_all) return jsonify(items) @@ -476,7 +481,7 @@ def deletePhotos(): @api.route("/api/communes", methods=["GET"]) def returnAllcommunes(): - get_all_communes = models.Communes.query.order_by("nom_commune").all() + get_all_communes = (models.Communes.query.join(models.CommunesTranslation).order_by(models.CommunesTranslation.nom_commune).all()) communes = models.CommunesSchema(many=True).dump(get_all_communes) return jsonify(communes), 200 diff --git a/backend/migrations/versions/d7fd422e1054_translations.py b/backend/migrations/versions/d7fd422e1054_translations.py new file mode 100644 index 00000000..4654b92a --- /dev/null +++ b/backend/migrations/versions/d7fd422e1054_translations.py @@ -0,0 +1,223 @@ +"""translations + +Revision ID: d7fd422e1054 +Revises: 4a02bd35bb30 +Create Date: 2024-10-22 12:20:06.196024 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd7fd422e1054' +down_revision = '4a02bd35bb30' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('lang', + sa.Column('id', sa.String(), nullable=False), + sa.Column('label', sa.String(), nullable=True), + sa.Column('is_published', sa.Boolean(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True, default=False), + sa.PrimaryKeyConstraint('id'), + sa.CheckConstraint( + "is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", + name="unique_default_lang" + ), + schema='geopaysages' + ) + # Insert default lang 'fr' + op.execute(sa.text(""" + INSERT INTO geopaysages.lang (id, label) + VALUES ('fr', 'Français') + """)) + + op.create_table('communes_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('nom_commune', sa.String(), nullable=True), + sa.Column('row_id', sa.String(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='communes_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.communes.code_commune'], name='commune_code_commune'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + # Insert existing communes in translation table + op.execute(sa.text(""" + INSERT INTO geopaysages.communes_translation (nom_commune, row_id, lang_id) + SELECT nom_commune, code_commune, 'fr' + FROM geopaysages.communes + """)) + + op.create_table('dico_stheme_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_stheme', sa.String(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='dico_stheme_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.dico_stheme.id_stheme'], name='stheme_id_stheme'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.dico_stheme_translation (name_stheme, row_id, lang_id) + SELECT name_stheme, id_stheme, 'fr' + FROM geopaysages.dico_stheme + """)) + + op.create_table('dico_theme_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_theme', sa.String(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='dico_theme_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.dico_theme.id_theme'], name='theme_id_theme'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.dico_theme_translation (name_theme, row_id, lang_id) + SELECT name_theme, id_theme, 'fr' + FROM geopaysages.dico_theme + """)) + + op.create_table('t_observatory_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=True), + sa.Column('is_published', sa.Boolean(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='t_observatory_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.t_observatory.id'], name='observatory_id'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.t_observatory_translation (title, is_published, row_id, lang_id) + SELECT title, is_published, id, 'fr' + FROM geopaysages.t_observatory + """)) + + op.create_table('t_site_translation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_site', sa.String(), nullable=True), + sa.Column('desc_site', sa.String(), nullable=True), + sa.Column('legend_site', sa.String(), nullable=True), + sa.Column('publish_site', sa.Boolean(), nullable=True), + sa.Column('row_id', sa.Integer(), nullable=True), + sa.Column('lang_id', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['lang_id'], ['geopaysages.lang.id'], name='t_site_translation_fk_lang'), + sa.ForeignKeyConstraint(['row_id'], ['geopaysages.t_site.id_site'], name='site_id_site'), + sa.PrimaryKeyConstraint('id'), + schema='geopaysages' + ) + op.execute(sa.text(""" + INSERT INTO geopaysages.t_site_translation (name_site, desc_site, legend_site, publish_site, row_id, lang_id) + SELECT name_site, desc_site, legend_site, publish_site, id_site, 'fr' + FROM geopaysages.t_site + """)) + + op.drop_column('communes', 'nom_commune', schema='geopaysages') + op.drop_column('dico_stheme', 'name_stheme', schema='geopaysages') + op.drop_column('dico_theme', 'name_theme', schema='geopaysages') + op.drop_column('t_observatory', 'title', schema='geopaysages') + op.drop_column('t_observatory', 'is_published', schema='geopaysages') + op.drop_column('t_site', 'publish_site', schema='geopaysages') + op.drop_column('t_site', 'name_site', schema='geopaysages') + op.drop_column('t_site', 'desc_site', schema='geopaysages') + op.drop_column('t_site', 'legend_site', schema='geopaysages') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('t_site', sa.Column('legend_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('desc_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('name_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('publish_site', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_observatory', sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_observatory', sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('dico_theme', sa.Column('name_theme', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('dico_stheme', sa.Column('name_stheme', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('communes', sa.Column('nom_commune', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + + + # populate + op.execute(sa.text(""" + UPDATE geopaysages.communes c + SET nom_commune = ( + SELECT ct.nom_commune + FROM geopaysages.communes_translation ct + WHERE ct.row_id = c.code_commune AND ct.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.dico_stheme d + SET name_stheme = ( + SELECT dt.name_stheme + FROM geopaysages.dico_stheme_translation dt + WHERE d.id_stheme = dt.row_id AND dt.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.dico_theme d + SET name_theme = ( + SELECT dt.name_theme + FROM geopaysages.dico_theme_translation dt + WHERE d.id_theme = dt.row_id AND dt.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.t_observatory o + SET + title = ( + SELECT ot.title + FROM geopaysages.t_observatory_translation ot + WHERE o.id = ot.row_id AND ot.lang_id = 'fr' + ), + is_published = ( + SELECT ot.is_published + FROM geopaysages.t_observatory_translation ot + WHERE o.id = ot.row_id AND ot.lang_id = 'fr' + ) + """)) + + op.execute(sa.text(""" + UPDATE geopaysages.t_site s + SET + legend_site = ( + SELECT st.legend_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), + desc_site = ( + SELECT st.desc_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), + name_site = ( + SELECT st.name_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), + publish_site = ( + SELECT st.publish_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ) + """)) + + op.drop_table('t_site_translation', schema='geopaysages') + op.drop_table('t_observatory_translation', schema='geopaysages') + op.drop_table('dico_theme_translation', schema='geopaysages') + op.drop_table('dico_stheme_translation', schema='geopaysages') + op.drop_table('communes_translation', schema='geopaysages') + op.drop_table('lang', schema='geopaysages') + # ### end Alembic commands ### diff --git a/backend/models.py b/backend/models.py index 820c0eea..2b368920 100644 --- a/backend/models.py +++ b/backend/models.py @@ -17,6 +17,23 @@ class Conf(db.Model): key = db.Column(db.String, primary_key=True) value = db.Column(db.String) + +class Lang(db.Model): + __tablename__ = 'lang' + __table_args__ = ( + {'schema': 'geopaysages'}, + db.CheckConstraint("is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", name="unique_default_lang") + ) + + id = db.Column(db.String, primary_key=True) + label = db.Column(db.String) + is_published = db.Column(db.Boolean) + is_default = db.Column(db.Boolean, default=False) + observatory_translations = db.relationship('ObservatoryTranslation', back_populates='lang') + site_translations = db.relationship('TSiteTranslation', back_populates='lang') + dico_stheme_translations = db.relationship('DicoSthemeTranslation', back_populates='lang') + dico_theme_translations = db.relationship('DicoThemeTranslation', back_populates='lang') + communes_translations = db.relationship('CommunesTranslation', back_populates='lang') class ComparatorEnum(Enum): @@ -28,15 +45,31 @@ class Observatory(db.Model): __tablename__ = "t_observatory" __table_args__ = {"schema": "geopaysages"} - id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) - title = db.Column(db.String) + id = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) ref = db.Column(db.String) color = db.Column(db.String) thumbnail = db.Column(db.String) logo = db.Column(db.String) comparator = db.Column(db.Enum(ComparatorEnum, name="comparator_enum")) - geom = db.Column(Geometry(geometry_type="MULTIPOLYGON", srid=4326)) + geom = db.Column(Geometry(geometry_type='MULTIPOLYGON', srid=4326)) + translations = db.relationship('ObservatoryTranslation', back_populates='row', lazy=True) + +class ObservatoryTranslation(db.Model): + __tablename__ = 't_observatory_translation' + __table_args__ = {'schema': 'geopaysages'} + + id = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) + title = db.Column(db.String) is_published = db.Column(db.Boolean) + row_id = db.Column(db.ForeignKey( + 'geopaysages.t_observatory.id', name='observatory_id')) + row = db.relationship('Observatory', back_populates='translations') + lang_id = db.Column(db.ForeignKey( + 'geopaysages.lang.id', name='t_observatory_translation_fk_lang')) + lang = db.relationship('Lang', primaryjoin='ObservatoryTranslation.lang_id == Lang.id') + class TSite(db.Model): @@ -48,28 +81,34 @@ class TSite(db.Model): db.ForeignKey("geopaysages.t_observatory.id", name="t_site_fk_observatory") ) observatory = db.relationship( - "Observatory", primaryjoin="TSite.id_observatory == Observatory.id" - ) - name_site = db.Column(db.String) + 'Observatory', primaryjoin='TSite.id_observatory == Observatory.id') ref_site = db.Column(db.String) - desc_site = db.Column(db.String) - legend_site = db.Column(db.String) testim_site = db.Column(db.String) code_city_site = db.Column(db.String) alti_site = db.Column(db.Integer) path_file_guide_site = db.Column(db.String) - publish_site = db.Column(db.Boolean) - geom = db.Column(Geometry(geometry_type="POINT", srid=4326)) + geom = db.Column(Geometry(geometry_type='POINT', srid=4326)) main_photo = db.Column(db.Integer) main_theme_id = db.Column(db.ForeignKey("geopaysages.dico_theme.id_theme")) main_theme = db.relationship( - "DicoTheme", primaryjoin="TSite.main_theme_id == DicoTheme.id_theme" - ) + 'DicoTheme', primaryjoin='TSite.main_theme_id == DicoTheme.id_theme') + __tablename__ = 't_site_translation' + __table_args__ = {'schema': 'geopaysages'} + + id = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) + name_site = db.Column(db.String) + desc_site = db.Column(db.String) + legend_site = db.Column(db.String) + publish_site = db.Column(db.Boolean) + row_id = db.Column(db.ForeignKey('geopaysages.t_site.id_site', name='site_id_site')) + row = db.relationship('TSite', back_populates='translations') + lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='t_site_translation_fk_lang')) + lang = db.relationship('Lang', back_populates='site_translations') class CorSiteSthemeTheme(db.Model): __tablename__ = "cor_site_stheme_theme" - __table_args__ = {"schema": "geopaysages"} id_site_stheme_theme = db.Column( db.Integer, nullable=False, server_default=db.FetchedValue() @@ -140,19 +179,44 @@ class DicoStheme(db.Model): __tablename__ = "dico_stheme" __table_args__ = {"schema": "geopaysages"} - id_stheme = db.Column( - db.Integer, primary_key=True, server_default=db.FetchedValue() - ) - name_stheme = db.Column(db.String) + id_stheme = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) + translations = db.relationship('DicoSthemeTranslation', back_populates='row', lazy=True) +class DicoSthemeTranslation(db.Model): + __tablename__ = 'dico_stheme_translation' + __table_args__ = {'schema': 'geopaysages'} + id = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) + name_stheme = db.Column(db.String) + row_id = db.Column(db.ForeignKey('geopaysages.dico_stheme.id_stheme', name='stheme_id_stheme')) + row = db.relationship('DicoStheme', back_populates='translations') + lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='dico_stheme_translation_fk_lang')) + lang = db.relationship('Lang', back_populates='dico_stheme_translations') + + class DicoTheme(db.Model): __tablename__ = "dico_theme" __table_args__ = {"schema": "geopaysages"} - id_theme = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) - name_theme = db.Column(db.String) + id_theme = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) icon = db.Column(db.String) + translations = db.relationship('DicoThemeTranslation', back_populates='row', lazy=True) + + +class DicoThemeTranslation(db.Model): + __tablename__ = 'dico_theme_translation' + __table_args__ = {'schema': 'geopaysages'} + + id = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) + name_theme = db.Column(db.String) + row_id = db.Column(db.ForeignKey('geopaysages.dico_theme.id_theme', name='theme_id_theme')) + row = db.relationship('DicoTheme', back_populates='translations') + lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='dico_theme_translation_fk_lang')) + lang = db.relationship('Lang', back_populates='dico_theme_translations') class TRole(db.Model): @@ -230,10 +294,21 @@ class Communes(db.Model): __tablename__ = "communes" __table_args__ = {"schema": "geopaysages"} - code_commune = db.Column( - db.String, primary_key=True, server_default=db.FetchedValue() - ) + code_commune = db.Column(db.String, primary_key=True, + server_default=db.FetchedValue()) + translations = db.relationship('CommunesTranslation', back_populates='row', lazy=True) + +class CommunesTranslation(db.Model): + __tablename__ = 'communes_translation' + __table_args__ = {'schema': 'geopaysages'} + + id = db.Column(db.Integer, primary_key=True, + server_default=db.FetchedValue()) nom_commune = db.Column(db.String) + row_id = db.Column(db.ForeignKey('geopaysages.communes.code_commune', name='commune_code_commune')) + row = db.relationship('Communes', back_populates='translations') + lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='communes_translation_fk_lang')) + lang = db.relationship('Lang', back_populates='communes_translations') class GeographySerializationField(fields.String): @@ -265,13 +340,57 @@ def _deserialize(self, value, attr, data): # schemas# +class TranslationSchema(ma.SQLAlchemyAutoSchema): + class Meta: + fields = ('lang_id', 'title', 'is_published') + +class LangSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Lang + fields = ('id', 'label') + + +class CommunesTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + class Meta: + model = CommunesTranslation + fields = ('nom_commune', 'lang_id', 'lang') + +class ObservatoryTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + class Meta: + model = ObservatoryTranslation + fields = ('title','is_published','lang_id') + +class TSiteTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + class Meta: + model = TSiteTranslation + fields = ('name_site', 'desc_site', 'legend_site', 'publish_site', 'lang_id') + +class DicoThemeTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + class Meta: + model = DicoThemeTranslation + fields = ('name_theme', 'lang_id', 'lang') + +class DicoSthemeTranslationSchema(ma.SQLAlchemyAutoSchema): + lang = ma.Nested(LangSchema) + class Meta: + model = DicoSthemeTranslation + fields = ('name_stheme', 'lang_id', 'lang') class DicoThemeSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(DicoThemeTranslationSchema, many=True) + class Meta: - fields = ("id_theme", "name_theme", "icon") + model = DicoTheme + fields = ('id_theme', 'icon', 'translations') class DicoSthemeSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(DicoSthemeTranslationSchema, many=True) + class Meta: model = DicoStheme include_relationships = True @@ -311,6 +430,7 @@ class Meta: class ObservatorySchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(ObservatoryTranslationSchema, many=True) comparator = EnumField(ComparatorEnum, by_value=True) geom = fields.Method("geomSerialize") @@ -327,26 +447,16 @@ class Meta: include_relationships = True -class ObservatorySchemaFull(ma.SQLAlchemyAutoSchema): - comparator = EnumField(ComparatorEnum, by_value=True) - geom = fields.Method("geomSerialize") - +class ObservatorySchemaFull(ObservatorySchema): @staticmethod def geomSerialize(obj): if obj.geom is None: return None p = to_shape(obj.geom) return p.wkt - - class Meta: - model = Observatory - include_relationships = True - - -class ObservatorySchemaLite(ma.SQLAlchemyAutoSchema): +class ObservatorySchemaLite(ObservatorySchema): comparator = EnumField(ComparatorEnum, by_value=False) - geom = fields.Method("geomSerialize") - + @staticmethod def geomSerialize(obj): if obj.geom is None: @@ -355,18 +465,13 @@ def geomSerialize(obj): s = p.simplify(0.001, preserve_topology=True) return s.wkt - class Meta: - model = Observatory - include_relationships = True - class TSiteSchema(ma.SQLAlchemyAutoSchema): - geom = GeographySerializationField(attribute="geom") - observatory = ma.Nested( - ObservatorySchema, only=["id", "title", "ref", "color", "logo"] - ) - main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "name_theme", "icon"]) - + translations = ma.Nested(TSiteTranslationSchema, many=True) + geom = GeographySerializationField(attribute='geom') + observatory = ma.Nested(ObservatorySchema, only=["id", "title", "ref", "color", "logo"]) + main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) + class Meta: model = TSite include_fk = True @@ -374,5 +479,8 @@ class Meta: class CommunesSchema(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(CommunesTranslationSchema, many=True) class Meta: model = Communes + include_relationships = True + From b8a8f4e6af8bec2e9630a1b595ba4b7c58f7cb05 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 12:49:06 +0200 Subject: [PATCH 023/107] feat: add is_published and is_default unique to lang table --- .../versions/d7fd422e1054_translations.py | 20 +- backend/models.py | 241 +++++++++++------- 2 files changed, 161 insertions(+), 100 deletions(-) diff --git a/backend/migrations/versions/d7fd422e1054_translations.py b/backend/migrations/versions/d7fd422e1054_translations.py index 4654b92a..56636922 100644 --- a/backend/migrations/versions/d7fd422e1054_translations.py +++ b/backend/migrations/versions/d7fd422e1054_translations.py @@ -24,16 +24,13 @@ def upgrade(): sa.Column('is_published', sa.Boolean(), nullable=True), sa.Column('is_default', sa.Boolean(), nullable=True, default=False), sa.PrimaryKeyConstraint('id'), - sa.CheckConstraint( - "is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", - name="unique_default_lang" - ), + sa.UniqueConstraint('is_default', name='uq_default_lang'), schema='geopaysages' ) # Insert default lang 'fr' op.execute(sa.text(""" - INSERT INTO geopaysages.lang (id, label) - VALUES ('fr', 'Français') + INSERT INTO geopaysages.lang (id, label, is_published, is_default) + VALUES ('fr', 'Français', true, true) """)) op.create_table('communes_translation', @@ -106,6 +103,7 @@ def upgrade(): sa.Column('id', sa.Integer(), nullable=False), sa.Column('name_site', sa.String(), nullable=True), sa.Column('desc_site', sa.String(), nullable=True), + sa.Column('testim_site', sa.String(), nullable=True), sa.Column('legend_site', sa.String(), nullable=True), sa.Column('publish_site', sa.Boolean(), nullable=True), sa.Column('row_id', sa.Integer(), nullable=True), @@ -116,8 +114,8 @@ def upgrade(): schema='geopaysages' ) op.execute(sa.text(""" - INSERT INTO geopaysages.t_site_translation (name_site, desc_site, legend_site, publish_site, row_id, lang_id) - SELECT name_site, desc_site, legend_site, publish_site, id_site, 'fr' + INSERT INTO geopaysages.t_site_translation (name_site, desc_site, testim_site, legend_site, publish_site, row_id, lang_id) + SELECT name_site, desc_site, testim_site, legend_site, publish_site, id_site, 'fr' FROM geopaysages.t_site """)) @@ -137,6 +135,7 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.add_column('t_site', sa.Column('legend_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_site', sa.Column('desc_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') + op.add_column('t_site', sa.Column('testim_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_site', sa.Column('name_site', sa.VARCHAR(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_site', sa.Column('publish_site', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') op.add_column('t_observatory', sa.Column('is_published', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='geopaysages') @@ -202,6 +201,11 @@ def downgrade(): FROM geopaysages.t_site_translation st WHERE s.id_site = st.row_id AND st.lang_id = 'fr' ), + testim_site = ( + SELECT st.testim_site + FROM geopaysages.t_site_translation st + WHERE s.id_site = st.row_id AND st.lang_id = 'fr' + ), name_site = ( SELECT st.name_site FROM geopaysages.t_site_translation st diff --git a/backend/models.py b/backend/models.py index 2b368920..2fa0842f 100644 --- a/backend/models.py +++ b/backend/models.py @@ -17,23 +17,36 @@ class Conf(db.Model): key = db.Column(db.String, primary_key=True) value = db.Column(db.String) - + + class Lang(db.Model): - __tablename__ = 'lang' + __tablename__ = "lang" __table_args__ = ( - {'schema': 'geopaysages'}, - db.CheckConstraint("is_default IS NOT TRUE OR (is_default IS TRUE AND id IN (SELECT id FROM geopaysages.lang WHERE is_default IS TRUE HAVING COUNT(*) = 1))", name="unique_default_lang") + {"schema": "geopaysages"}, ) - + id = db.Column(db.String, primary_key=True) label = db.Column(db.String) is_published = db.Column(db.Boolean) is_default = db.Column(db.Boolean, default=False) - observatory_translations = db.relationship('ObservatoryTranslation', back_populates='lang') - site_translations = db.relationship('TSiteTranslation', back_populates='lang') - dico_stheme_translations = db.relationship('DicoSthemeTranslation', back_populates='lang') - dico_theme_translations = db.relationship('DicoThemeTranslation', back_populates='lang') - communes_translations = db.relationship('CommunesTranslation', back_populates='lang') + __table_args__ = ( + db.UniqueConstraint('is_default', name='uq_default_lang'), + {"schema": "geopaysages"}, + ) + observatory_translations = db.relationship( + "ObservatoryTranslation", back_populates="lang" + ) + site_translations = db.relationship("TSiteTranslation", back_populates="lang") + dico_stheme_translations = db.relationship( + "DicoSthemeTranslation", back_populates="lang" + ) + dico_theme_translations = db.relationship( + "DicoThemeTranslation", back_populates="lang" + ) + communes_translations = db.relationship( + "CommunesTranslation", back_populates="lang" + ) + class ComparatorEnum(Enum): @@ -45,32 +58,35 @@ class Observatory(db.Model): __tablename__ = "t_observatory" __table_args__ = {"schema": "geopaysages"} - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) ref = db.Column(db.String) color = db.Column(db.String) thumbnail = db.Column(db.String) logo = db.Column(db.String) comparator = db.Column(db.Enum(ComparatorEnum, name="comparator_enum")) - geom = db.Column(Geometry(geometry_type='MULTIPOLYGON', srid=4326)) - translations = db.relationship('ObservatoryTranslation', back_populates='row', lazy=True) + geom = db.Column(Geometry(geometry_type="MULTIPOLYGON", srid=4326)) + translations = db.relationship( + "ObservatoryTranslation", back_populates="row", lazy=True + ) + class ObservatoryTranslation(db.Model): - __tablename__ = 't_observatory_translation' - __table_args__ = {'schema': 'geopaysages'} - - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + __tablename__ = "t_observatory_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) title = db.Column(db.String) is_published = db.Column(db.Boolean) - row_id = db.Column(db.ForeignKey( - 'geopaysages.t_observatory.id', name='observatory_id')) - row = db.relationship('Observatory', back_populates='translations') - lang_id = db.Column(db.ForeignKey( - 'geopaysages.lang.id', name='t_observatory_translation_fk_lang')) - lang = db.relationship('Lang', primaryjoin='ObservatoryTranslation.lang_id == Lang.id') - - + row_id = db.Column( + db.ForeignKey("geopaysages.t_observatory.id", name="observatory_id") + ) + row = db.relationship("Observatory", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="t_observatory_translation_fk_lang") + ) + lang = db.relationship( + "Lang", primaryjoin="ObservatoryTranslation.lang_id == Lang.id" + ) class TSite(db.Model): __tablename__ = "t_site" @@ -81,34 +97,42 @@ class TSite(db.Model): db.ForeignKey("geopaysages.t_observatory.id", name="t_site_fk_observatory") ) observatory = db.relationship( - 'Observatory', primaryjoin='TSite.id_observatory == Observatory.id') + "Observatory", primaryjoin="TSite.id_observatory == Observatory.id" + ) ref_site = db.Column(db.String) - testim_site = db.Column(db.String) code_city_site = db.Column(db.String) alti_site = db.Column(db.Integer) path_file_guide_site = db.Column(db.String) - geom = db.Column(Geometry(geometry_type='POINT', srid=4326)) + geom = db.Column(Geometry(geometry_type="POINT", srid=4326)) main_photo = db.Column(db.Integer) main_theme_id = db.Column(db.ForeignKey("geopaysages.dico_theme.id_theme")) main_theme = db.relationship( - 'DicoTheme', primaryjoin='TSite.main_theme_id == DicoTheme.id_theme') - __tablename__ = 't_site_translation' - __table_args__ = {'schema': 'geopaysages'} + "DicoTheme", primaryjoin="TSite.main_theme_id == DicoTheme.id_theme" + ) + translations = db.relationship("TSiteTranslation", back_populates="row", lazy=True) + - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) +class TSiteTranslation(db.Model): + __tablename__ = "t_site_translation" + __table_args__ = {"schema": "geopaysages"} + + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) name_site = db.Column(db.String) desc_site = db.Column(db.String) + testim_site = db.Column(db.String) legend_site = db.Column(db.String) publish_site = db.Column(db.Boolean) - row_id = db.Column(db.ForeignKey('geopaysages.t_site.id_site', name='site_id_site')) - row = db.relationship('TSite', back_populates='translations') - lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='t_site_translation_fk_lang')) - lang = db.relationship('Lang', back_populates='site_translations') + row_id = db.Column(db.ForeignKey("geopaysages.t_site.id_site", name="site_id_site")) + row = db.relationship("TSite", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="t_site_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="site_translations") class CorSiteSthemeTheme(db.Model): __tablename__ = "cor_site_stheme_theme" + __table_args__ = {"schema": "geopaysages"} id_site_stheme_theme = db.Column( db.Integer, nullable=False, server_default=db.FetchedValue() @@ -179,44 +203,55 @@ class DicoStheme(db.Model): __tablename__ = "dico_stheme" __table_args__ = {"schema": "geopaysages"} - id_stheme = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) - translations = db.relationship('DicoSthemeTranslation', back_populates='row', lazy=True) + id_stheme = db.Column( + db.Integer, primary_key=True, server_default=db.FetchedValue() + ) + translations = db.relationship( + "DicoSthemeTranslation", back_populates="row", lazy=True + ) + class DicoSthemeTranslation(db.Model): - __tablename__ = 'dico_stheme_translation' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "dico_stheme_translation" + __table_args__ = {"schema": "geopaysages"} - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) name_stheme = db.Column(db.String) - row_id = db.Column(db.ForeignKey('geopaysages.dico_stheme.id_stheme', name='stheme_id_stheme')) - row = db.relationship('DicoStheme', back_populates='translations') - lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='dico_stheme_translation_fk_lang')) - lang = db.relationship('Lang', back_populates='dico_stheme_translations') - - + row_id = db.Column( + db.ForeignKey("geopaysages.dico_stheme.id_stheme", name="stheme_id_stheme") + ) + row = db.relationship("DicoStheme", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="dico_stheme_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="dico_stheme_translations") + + class DicoTheme(db.Model): __tablename__ = "dico_theme" __table_args__ = {"schema": "geopaysages"} - id_theme = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id_theme = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) icon = db.Column(db.String) - translations = db.relationship('DicoThemeTranslation', back_populates='row', lazy=True) + translations = db.relationship( + "DicoThemeTranslation", back_populates="row", lazy=True + ) class DicoThemeTranslation(db.Model): - __tablename__ = 'dico_theme_translation' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "dico_theme_translation" + __table_args__ = {"schema": "geopaysages"} - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) name_theme = db.Column(db.String) - row_id = db.Column(db.ForeignKey('geopaysages.dico_theme.id_theme', name='theme_id_theme')) - row = db.relationship('DicoTheme', back_populates='translations') - lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='dico_theme_translation_fk_lang')) - lang = db.relationship('Lang', back_populates='dico_theme_translations') + row_id = db.Column( + db.ForeignKey("geopaysages.dico_theme.id_theme", name="theme_id_theme") + ) + row = db.relationship("DicoTheme", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="dico_theme_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="dico_theme_translations") class TRole(db.Model): @@ -294,21 +329,28 @@ class Communes(db.Model): __tablename__ = "communes" __table_args__ = {"schema": "geopaysages"} - code_commune = db.Column(db.String, primary_key=True, - server_default=db.FetchedValue()) - translations = db.relationship('CommunesTranslation', back_populates='row', lazy=True) + code_commune = db.Column( + db.String, primary_key=True, server_default=db.FetchedValue() + ) + translations = db.relationship( + "CommunesTranslation", back_populates="row", lazy=True + ) + class CommunesTranslation(db.Model): - __tablename__ = 'communes_translation' - __table_args__ = {'schema': 'geopaysages'} + __tablename__ = "communes_translation" + __table_args__ = {"schema": "geopaysages"} - id = db.Column(db.Integer, primary_key=True, - server_default=db.FetchedValue()) + id = db.Column(db.Integer, primary_key=True, server_default=db.FetchedValue()) nom_commune = db.Column(db.String) - row_id = db.Column(db.ForeignKey('geopaysages.communes.code_commune', name='commune_code_commune')) - row = db.relationship('Communes', back_populates='translations') - lang_id = db.Column(db.ForeignKey('geopaysages.lang.id', name='communes_translation_fk_lang')) - lang = db.relationship('Lang', back_populates='communes_translations') + row_id = db.Column( + db.ForeignKey("geopaysages.communes.code_commune", name="commune_code_commune") + ) + row = db.relationship("Communes", back_populates="translations") + lang_id = db.Column( + db.ForeignKey("geopaysages.lang.id", name="communes_translation_fk_lang") + ) + lang = db.relationship("Lang", back_populates="communes_translations") class GeographySerializationField(fields.String): @@ -342,55 +384,66 @@ def _deserialize(self, value, attr, data): class TranslationSchema(ma.SQLAlchemyAutoSchema): class Meta: - fields = ('lang_id', 'title', 'is_published') + fields = ("lang_id", "title", "is_published") + class LangSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Lang - fields = ('id', 'label') - + fields = ("id", "label", "is_published", "is_default") + class CommunesTranslationSchema(ma.SQLAlchemyAutoSchema): lang = ma.Nested(LangSchema) + class Meta: model = CommunesTranslation - fields = ('nom_commune', 'lang_id', 'lang') + fields = ("nom_commune", "lang_id", "lang") + class ObservatoryTranslationSchema(ma.SQLAlchemyAutoSchema): - lang = ma.Nested(LangSchema) + lang = ma.Nested(LangSchema) + class Meta: model = ObservatoryTranslation - fields = ('title','is_published','lang_id') - + fields = ("title", "is_published", "lang_id") + + class TSiteTranslationSchema(ma.SQLAlchemyAutoSchema): lang = ma.Nested(LangSchema) + class Meta: model = TSiteTranslation - fields = ('name_site', 'desc_site', 'legend_site', 'publish_site', 'lang_id') + fields = ("name_site", "desc_site", "legend_site", "publish_site", "lang_id") + class DicoThemeTranslationSchema(ma.SQLAlchemyAutoSchema): lang = ma.Nested(LangSchema) + class Meta: model = DicoThemeTranslation - fields = ('name_theme', 'lang_id', 'lang') + fields = ("name_theme", "lang_id", "lang") + class DicoSthemeTranslationSchema(ma.SQLAlchemyAutoSchema): lang = ma.Nested(LangSchema) + class Meta: model = DicoSthemeTranslation - fields = ('name_stheme', 'lang_id', 'lang') + fields = ("name_stheme", "lang_id", "lang") + class DicoThemeSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(DicoThemeTranslationSchema, many=True) - + class Meta: model = DicoTheme - fields = ('id_theme', 'icon', 'translations') + fields = ("id_theme", "icon", "translations") class DicoSthemeSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(DicoSthemeTranslationSchema, many=True) - + class Meta: model = DicoStheme include_relationships = True @@ -454,9 +507,11 @@ def geomSerialize(obj): return None p = to_shape(obj.geom) return p.wkt + + class ObservatorySchemaLite(ObservatorySchema): comparator = EnumField(ComparatorEnum, by_value=False) - + @staticmethod def geomSerialize(obj): if obj.geom is None: @@ -468,10 +523,12 @@ def geomSerialize(obj): class TSiteSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(TSiteTranslationSchema, many=True) - geom = GeographySerializationField(attribute='geom') - observatory = ma.Nested(ObservatorySchema, only=["id", "title", "ref", "color", "logo"]) + geom = GeographySerializationField(attribute="geom") + observatory = ma.Nested( + ObservatorySchema, only=["id", "title", "ref", "color", "logo"] + ) main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) - + class Meta: model = TSite include_fk = True @@ -480,7 +537,7 @@ class Meta: class CommunesSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(CommunesTranslationSchema, many=True) + class Meta: model = Communes include_relationships = True - From 5caa13949e2d637b8d6b5c66d9840269d6a0fdd4 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 13:49:06 +0200 Subject: [PATCH 024/107] feat: add languages route --- backend/api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/api.py b/backend/api.py index 82d84eb1..979e191e 100644 --- a/backend/api.py +++ b/backend/api.py @@ -486,7 +486,14 @@ def returnAllcommunes(): return jsonify(communes), 200 -@api.route("/api/logout", methods=["GET"]) +@api.route('/api/languages', methods=['GET']) +def returnAllLanguages(): + get_all_languages = models.Lang.query.all() + languages = models.LangSchema(many=True).dump(get_all_languages) + return jsonify(languages), 200 + + +@api.route('/api/logout', methods=['GET']) def logout(): resp = Response("", 200) resp.delete_cookie("token") From e0e89766d53b73ef7523440d25259d6f3d77848e Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 15:50:13 +0200 Subject: [PATCH 025/107] chore: remove unused marshmallow schema --- backend/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/models.py b/backend/models.py index 2fa0842f..89f57554 100644 --- a/backend/models.py +++ b/backend/models.py @@ -382,10 +382,6 @@ def _deserialize(self, value, attr, data): # schemas# -class TranslationSchema(ma.SQLAlchemyAutoSchema): - class Meta: - fields = ("lang_id", "title", "is_published") - class LangSchema(ma.SQLAlchemyAutoSchema): class Meta: From 366923497577da87407c8891cee0eaf500a154d3 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Wed, 23 Oct 2024 17:11:50 +0200 Subject: [PATCH 026/107] style: code format --- backend/routes.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/backend/routes.py b/backend/routes.py index 315779b9..86fd3bfb 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -21,31 +21,37 @@ themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) communes_schema = models.CommunesSchema(many=True) + def localeGuard(f): @wraps(f) def decorated_function(*args, **kwargs): - locale = request.view_args.get('locale') + locale = request.view_args.get("locale") if not utils.isMultiLangs() and locale is not None: return redirect(url_for(request.endpoint)) if utils.isMultiLangs() and locale is None: - matched_locale = request.accept_languages.best_match(['fr', 'en']) + matched_locale = request.accept_languages.best_match(["fr", "en"]) if matched_locale is None: - return redirect('/') + return redirect("/") return redirect(url_for(request.endpoint, locale=matched_locale)) return f(*args, **kwargs) + return decorated_function + def homeLocaleSelector(): return "Select a language" -@main.route('/') -@main.route('//') + +@main.route("/") +@main.route("//") def home(locale=None): if utils.isMultiLangs() and locale is None: return homeLocaleSelector() if not utils.isMultiLangs() and locale is not None: - return redirect('/') - sql = text("SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6") + return redirect("/") + sql = text( + "SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6" + ) sites_proxy = db.engine.execute(sql).fetchall() sites = [dict(row.items()) for row in sites_proxy] @@ -239,8 +245,8 @@ def site_photos_last(id_site): return render_template("site_photo.jinja", site=site, photo=photo) -@main.route('/sites') -@main.route('//sites/') +@main.route("/sites") +@main.route("//sites/") @localeGuard def sites(locale=None): data = utils.getFiltersData() @@ -254,8 +260,8 @@ def sites(locale=None): ) -@main.route('/legal-notices/') -@main.route('//legal-notices/') +@main.route("/legal-notices/") +@main.route("//legal-notices/") @localeGuard def legal_notices(): From 455eb30173c714c27b9f72ded67d0c3c9b66cdd5 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Wed, 23 Oct 2024 17:40:34 +0200 Subject: [PATCH 027/107] feat: wip multi langs --- backend/models.py | 2 +- backend/routes.py | 45 +++++++++++++++++++++++++++++++++++++-------- backend/utils.py | 3 ++- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/backend/models.py b/backend/models.py index 89f57554..2911f16e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -521,7 +521,7 @@ class TSiteSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(TSiteTranslationSchema, many=True) geom = GeographySerializationField(attribute="geom") observatory = ma.Nested( - ObservatorySchema, only=["id", "title", "ref", "color", "logo"] + ObservatorySchema, only=["id", "ref", "color", "logo"] ) main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) diff --git a/backend/routes.py b/backend/routes.py index 86fd3bfb..3e558d47 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -49,8 +49,14 @@ def home(locale=None): return homeLocaleSelector() if not utils.isMultiLangs() and locale is not None: return redirect("/") + locale = utils.getLocale() sql = text( - "SELECT * FROM geopaysages.t_site p join geopaysages.t_observatory o on o.id=p.id_observatory where p.publish_site=true and o.is_published is true ORDER BY RANDOM() LIMIT 6" + """SELECT * FROM geopaysages.t_site p + join geopaysages.t_site_translation pt on p.id_site=pt.row_id and pt.lang_id = '{locale}' + join geopaysages.t_observatory o on o.id=p.id_observatory + join geopaysages.t_observatory_translation ot on o.id=ot.row_id and ot.lang_id = '{locale}' + where pt.publish_site=true and ot.is_published is true ORDER BY RANDOM() LIMIT 6 + """ ) sites_proxy = db.engine.execute(sql).fetchall() sites = [dict(row.items()) for row in sites_proxy] @@ -76,9 +82,14 @@ def home(locale=None): # WAHO tordu l'histoire! if len(sites_without_photo): sql_missing_photos_str = ( - "select distinct on (id_site) p.* from geopaysages.t_photo p join geopaysages.t_observatory o on o.id=p.id_observatory where p.id_site IN (" + "select distinct on (id_site) p.* from geopaysages.t_photo p " + + "join geopaysages.t_observatory o on o.id=p.id_observatory " + + "join geopaysages.t_observatory_translation ot on o.id=ot.row_id and ot.lang_id = '" + + locale + + "' " + + "where p.id_site IN (" + ",".join(sites_without_photo) - + ") and o.is_published is true order by id_site, filter_date desc" + + ") and ot.is_published is true order by id_site, filter_date desc" ) sql_missing_photos = text(sql_missing_photos_str) missing_photos_result = db.engine.execute(sql_missing_photos).fetchall() @@ -111,8 +122,20 @@ def home(locale=None): ) all_sites = site_schema.dump( - models.TSite.query.join(models.Observatory).filter( - models.TSite.publish_site == True, models.Observatory.is_published == True + models.TSite.query.join( + models.TSiteTranslation, + (models.TSite.id_site == models.TSiteTranslation.row_id) + & (models.TSiteTranslation.lang_id == locale), + ) + .join(models.Observatory) + .join( + models.ObservatoryTranslation, + (models.Observatory.id == models.ObservatoryTranslation.row_id) + & (models.ObservatoryTranslation.lang_id == locale), + ) + .filter( + models.TSiteTranslation.publish_site == True, + models.ObservatoryTranslation.is_published == True, ) ) @@ -122,9 +145,15 @@ def home(locale=None): carousel_photos = list(filter(lambda x: x != ".gitkeep", carousel_photos)) if utils.isMultiObservatories() == True: - observatories = models.Observatory.query.filter( - models.Observatory.is_published == True - ).order_by(models.Observatory.title) + observatories = ( + models.Observatory.query.join( + models.ObservatoryTranslation, + (models.Observatory.id == models.ObservatoryTranslation.row_id) + & (models.ObservatoryTranslation.lang_id == locale), + ) + .filter(models.ObservatoryTranslation.is_published == True) + .order_by(models.ObservatoryTranslation.title) + ) dump_observatories = observatory_schema_lite.dump(observatories) col_max = 5 diff --git a/backend/utils.py b/backend/utils.py index 45f75132..86954ed1 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -78,8 +78,9 @@ def getDbConf(): def isMultiObservatories(): + locale = getLocale() # Pourrait passer par un count sql - sql = text("SELECT id FROM geopaysages.t_observatory where is_published is true") + sql = text("SELECT o.id FROM geopaysages.t_observatory o join geopaysages.t_observatory_translation ot on o.id = ot.row_id and ot.lang_id = '{locale}' where ot.is_published is true") result = db.engine.execute(sql).fetchall() rows = [dict(row) for row in result] if len(rows) > 1: From d1295be034a4002a5fb9bca3c2f83e6cf39d1e57 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Thu, 24 Oct 2024 08:50:13 +0200 Subject: [PATCH 028/107] style: code format --- backend/api.py | 14 ++++++++------ backend/utils.py | 15 ++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/backend/api.py b/backend/api.py index 979e191e..95d8cdab 100644 --- a/backend/api.py +++ b/backend/api.py @@ -1,5 +1,4 @@ from flask import ( - Flask, request, Blueprint, Response, @@ -80,8 +79,7 @@ def returnDdConf(): @api.route("/api/observatories", methods=["GET"]) def returnAllObservatories(): get_all = ( - models.Observatory.query - .join(models.ObservatoryTranslation) + models.Observatory.query.join(models.ObservatoryTranslation) .order_by(models.ObservatoryTranslation.title) .all() ) @@ -481,19 +479,23 @@ def deletePhotos(): @api.route("/api/communes", methods=["GET"]) def returnAllcommunes(): - get_all_communes = (models.Communes.query.join(models.CommunesTranslation).order_by(models.CommunesTranslation.nom_commune).all()) + get_all_communes = ( + models.Communes.query.join(models.CommunesTranslation) + .order_by(models.CommunesTranslation.nom_commune) + .all() + ) communes = models.CommunesSchema(many=True).dump(get_all_communes) return jsonify(communes), 200 -@api.route('/api/languages', methods=['GET']) +@api.route("/api/languages", methods=["GET"]) def returnAllLanguages(): get_all_languages = models.Lang.query.all() languages = models.LangSchema(many=True).dump(get_all_languages) return jsonify(languages), 200 -@api.route('/api/logout', methods=['GET']) +@api.route("/api/logout", methods=["GET"]) def logout(): resp = Response("", 200) resp.delete_cookie("token") diff --git a/backend/utils.py b/backend/utils.py index 86954ed1..24e752b5 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -21,16 +21,19 @@ site_schema = models.TSiteSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) + def getLocale(): - return request.view_args.get('locale', 'fr') + return request.view_args.get("locale", "fr") + def isMultiLangs(): return False + def getCustomTpl(name): - tpl_local = f'custom/{name}_{getLocale()}.jinja' - tpl_common = f'custom/{name}.jinja' - if os.path.exists(f'tpl/{tpl_local}'): + tpl_local = f"custom/{name}_{getLocale()}.jinja" + tpl_common = f"custom/{name}.jinja" + if os.path.exists(f"tpl/{tpl_local}"): return tpl_local if os.path.exists(f"tpl/{tpl_common}"): return tpl_common @@ -80,7 +83,9 @@ def getDbConf(): def isMultiObservatories(): locale = getLocale() # Pourrait passer par un count sql - sql = text("SELECT o.id FROM geopaysages.t_observatory o join geopaysages.t_observatory_translation ot on o.id = ot.row_id and ot.lang_id = '{locale}' where ot.is_published is true") + sql = text( + "SELECT o.id FROM geopaysages.t_observatory o join geopaysages.t_observatory_translation ot on o.id = ot.row_id and ot.lang_id = '{locale}' where ot.is_published is true" + ) result = db.engine.execute(sql).fetchall() rows = [dict(row) for row in result] if len(rows) > 1: From d803d14f898a1e0d363417d80f2069452e5ae74a Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Thu, 24 Oct 2024 09:06:08 +0200 Subject: [PATCH 029/107] feat: wip multi langs --- backend/models.py | 10 +++------- backend/routes.py | 2 +- backend/utils.py | 12 +++++++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/backend/models.py b/backend/models.py index 2911f16e..f100d487 100644 --- a/backend/models.py +++ b/backend/models.py @@ -21,16 +21,14 @@ class Conf(db.Model): class Lang(db.Model): __tablename__ = "lang" - __table_args__ = ( - {"schema": "geopaysages"}, - ) + __table_args__ = ({"schema": "geopaysages"},) id = db.Column(db.String, primary_key=True) label = db.Column(db.String) is_published = db.Column(db.Boolean) is_default = db.Column(db.Boolean, default=False) __table_args__ = ( - db.UniqueConstraint('is_default', name='uq_default_lang'), + db.UniqueConstraint("is_default", name="uq_default_lang"), {"schema": "geopaysages"}, ) observatory_translations = db.relationship( @@ -520,9 +518,7 @@ def geomSerialize(obj): class TSiteSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(TSiteTranslationSchema, many=True) geom = GeographySerializationField(attribute="geom") - observatory = ma.Nested( - ObservatorySchema, only=["id", "ref", "color", "logo"] - ) + observatory = ma.Nested(ObservatorySchema, only=["id", "ref", "color", "logo"]) main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) class Meta: diff --git a/backend/routes.py b/backend/routes.py index 3e558d47..4954a70d 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -51,7 +51,7 @@ def home(locale=None): return redirect("/") locale = utils.getLocale() sql = text( - """SELECT * FROM geopaysages.t_site p + f"""SELECT * FROM geopaysages.t_site p join geopaysages.t_site_translation pt on p.id_site=pt.row_id and pt.lang_id = '{locale}' join geopaysages.t_observatory o on o.id=p.id_observatory join geopaysages.t_observatory_translation ot on o.id=ot.row_id and ot.lang_id = '{locale}' diff --git a/backend/utils.py b/backend/utils.py index 24e752b5..cd9dd3ee 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -82,13 +82,15 @@ def getDbConf(): def isMultiObservatories(): locale = getLocale() - # Pourrait passer par un count sql sql = text( - "SELECT o.id FROM geopaysages.t_observatory o join geopaysages.t_observatory_translation ot on o.id = ot.row_id and ot.lang_id = '{locale}' where ot.is_published is true" + f"""SELECT count(*) + FROM geopaysages.t_observatory o + join geopaysages.t_observatory_translation ot on o.id = ot.row_id and ot.lang_id = '{locale}' + where ot.is_published is true""" ) - result = db.engine.execute(sql).fetchall() - rows = [dict(row) for row in result] - if len(rows) > 1: + result = db.engine.execute(sql) + count = result.scalar() + if count > 1: return True return False From b701c38fbbb51e2fa3ab3fab8e9c2e6f82c885ed Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Thu, 24 Oct 2024 18:06:34 +0200 Subject: [PATCH 030/107] feat: wip multi langs --- backend/models.py | 101 +++++++++++++++++++++++++++++++++++++++++++--- backend/routes.py | 16 ++++---- backend/utils.py | 53 ++++++++++++++++-------- 3 files changed, 142 insertions(+), 28 deletions(-) diff --git a/backend/models.py b/backend/models.py index f100d487..7bae60c1 100644 --- a/backend/models.py +++ b/backend/models.py @@ -2,7 +2,7 @@ from geoalchemy2.types import Geometry import geoalchemy2.functions as geo_funcs from geoalchemy2.shape import to_shape -from marshmallow import fields +from marshmallow import fields, post_dump from marshmallow_enum import EnumField from shapely.geometry import mapping @@ -381,6 +381,16 @@ def _deserialize(self, value, attr, data): # schemas# +def get_translated_data(self, data): + if not self.lang_id: + return data + for field in self.translatable_fields: + for translation in data["translations"]: + if translation["lang_id"] == self.lang_id: + data[field] = translation[field] + return data + + class LangSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Lang @@ -430,6 +440,16 @@ class Meta: class DicoThemeSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(DicoThemeTranslationSchema, many=True) + translatable_fields = DicoThemeTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + return get_translated_data(self, data) + class Meta: model = DicoTheme fields = ("id_theme", "icon", "translations") @@ -438,6 +458,16 @@ class Meta: class DicoSthemeSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(DicoSthemeTranslationSchema, many=True) + translatable_fields = DicoSthemeTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + return get_translated_data(self, data) + class Meta: model = DicoStheme include_relationships = True @@ -468,8 +498,8 @@ class Meta: class CorSthemeThemeSchema(ma.SQLAlchemyAutoSchema): - dico_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "name_theme"]) - dico_stheme = ma.Nested(DicoSthemeSchema, only=["id_stheme", "name_stheme"]) + dico_theme = ma.Nested(DicoThemeSchema, only=["id_theme"]) + dico_stheme = ma.Nested(DicoSthemeSchema, only=["id_stheme"]) class Meta: fields = ("dico_theme", "dico_stheme") @@ -481,6 +511,16 @@ class ObservatorySchema(ma.SQLAlchemyAutoSchema): comparator = EnumField(ComparatorEnum, by_value=True) geom = fields.Method("geomSerialize") + translatable_fields = ObservatoryTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + return get_translated_data(self, data) + @staticmethod def geomSerialize(obj): if obj.geom is None: @@ -494,7 +534,21 @@ class Meta: include_relationships = True -class ObservatorySchemaFull(ObservatorySchema): +class ObservatorySchemaFull(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(ObservatoryTranslationSchema, many=True) + comparator = EnumField(ComparatorEnum, by_value=True) + geom = fields.Method("geomSerialize") + + translatable_fields = ObservatoryTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + return get_translated_data(self, data) + @staticmethod def geomSerialize(obj): if obj.geom is None: @@ -503,8 +557,21 @@ def geomSerialize(obj): return p.wkt -class ObservatorySchemaLite(ObservatorySchema): +class ObservatorySchemaLite(ma.SQLAlchemyAutoSchema): + translations = ma.Nested(ObservatoryTranslationSchema, many=True) comparator = EnumField(ComparatorEnum, by_value=False) + geom = fields.Method("geomSerialize") + + translatable_fields = ObservatoryTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + print("translate_fields", self.lang_id) + return get_translated_data(self, data) @staticmethod def geomSerialize(obj): @@ -513,6 +580,10 @@ def geomSerialize(obj): p = to_shape(obj.geom) s = p.simplify(0.001, preserve_topology=True) return s.wkt + + class Meta: + model = Observatory + include_relationships = True class TSiteSchema(ma.SQLAlchemyAutoSchema): @@ -521,6 +592,16 @@ class TSiteSchema(ma.SQLAlchemyAutoSchema): observatory = ma.Nested(ObservatorySchema, only=["id", "ref", "color", "logo"]) main_theme = ma.Nested(DicoThemeSchema, only=["id_theme", "translations", "icon"]) + translatable_fields = TSiteTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + return get_translated_data(self, data) + class Meta: model = TSite include_fk = True @@ -530,6 +611,16 @@ class Meta: class CommunesSchema(ma.SQLAlchemyAutoSchema): translations = ma.Nested(CommunesTranslationSchema, many=True) + translatable_fields = CommunesTranslationSchema.Meta.fields + + def __init__(self, *args, **kwargs): + self.lang_id = kwargs.pop("locale", None) + super().__init__(*args, **kwargs) + + @post_dump + def translate_fields(self, data, **kwargs): + return get_translated_data(self, data) + class Meta: model = Communes include_relationships = True diff --git a/backend/routes.py b/backend/routes.py index 4954a70d..6b56374e 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -13,13 +13,8 @@ from env import db -dicotheme_schema = models.DicoThemeSchema(many=True) -dicostheme_schema = models.DicoSthemeSchema(many=True) photo_schema = models.TPhotoSchema(many=True) -observatory_schema_lite = models.ObservatorySchemaLite(many=True) -site_schema = models.TSiteSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) -communes_schema = models.CommunesSchema(many=True) def localeGuard(f): @@ -50,6 +45,8 @@ def home(locale=None): if not utils.isMultiLangs() and locale is not None: return redirect("/") locale = utils.getLocale() + site_schema = models.TSiteSchema(many=True, locale=locale) + communes_schema = models.CommunesSchema(many=True, locale=locale) sql = text( f"""SELECT * FROM geopaysages.t_site p join geopaysages.t_site_translation pt on p.id_site=pt.row_id and pt.lang_id = '{locale}' @@ -154,6 +151,7 @@ def home(locale=None): .filter(models.ObservatoryTranslation.is_published == True) .order_by(models.ObservatoryTranslation.title) ) + observatory_schema_lite = models.ObservatorySchemaLite(many=True, locale=locale) dump_observatories = observatory_schema_lite.dump(observatories) col_max = 5 @@ -191,7 +189,11 @@ def gallery(): @main.route("/sites/") -def site(id_site): +@main.route("//sites/") +@localeGuard +def site(id_site, locale): + site_schema = models.TSiteSchema(many=True, locale=locale) + communes_schema = models.CommunesSchema(many=True, locale=locale) get_site_by_id = models.TSite.query.filter_by(id_site=id_site, publish_site=True) site = site_schema.dump(get_site_by_id) if len(site) == 0: @@ -249,6 +251,7 @@ def getPhoto(photo): @main.route("/sites//photos/latest") def site_photos_last(id_site): + site_schema = models.TSiteSchema(many=True) get_site_by_id = models.TSite.query.filter_by(id_site=id_site, publish_site=True) site = site_schema.dump(get_site_by_id) if len(site) == 0: @@ -279,7 +282,6 @@ def site_photos_last(id_site): @localeGuard def sites(locale=None): data = utils.getFiltersData() - print(locale) return render_template( "sites.jinja", diff --git a/backend/utils.py b/backend/utils.py index cd9dd3ee..5e8a711f 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -14,11 +14,7 @@ db = SQLAlchemy() -dicotheme_schema = models.DicoThemeSchema(many=True) -dicostheme_schema = models.DicoSthemeSchema(many=True) photo_schema = models.TPhotoSchema(many=True) -observatory_schema = models.ObservatorySchema(many=True) -site_schema = models.TSiteSchema(many=True) themes_sthemes_schema = models.CorSthemeThemeSchema(many=True) @@ -75,7 +71,9 @@ def getDbConf(): except Exception as exception: conf[row.get("key")] = row.get("value") - conf["default_sort_sites"] = conf.get("default_sort_sites", "name_site") + conf["default_sort_sites"] = conf.get( + "default_sort_sites", "geopaysages.t_site_translation.name_site" + ) return conf @@ -95,14 +93,37 @@ def isMultiObservatories(): return False +def getLocalizedSitesQuery(): + locale = getLocale() + return ( + models.TSite.query.join( + models.TSiteTranslation, + (models.TSite.id_site == models.TSiteTranslation.row_id) + & (models.TSiteTranslation.lang_id == locale), + ) + .join(models.Observatory) + .join( + models.ObservatoryTranslation, + (models.Observatory.id == models.ObservatoryTranslation.row_id) + & (models.ObservatoryTranslation.lang_id == locale), + ) + .filter( + models.TSiteTranslation.publish_site == True, + models.ObservatoryTranslation.is_published == True, + ) + ) + + def getFiltersData(): dbconf = getDbConf() + locale = getLocale() + observatory_schema = models.ObservatorySchema(many=True, locale=locale) + site_schema = models.TSiteSchema(many=True, locale=locale) + dicotheme_schema = models.DicoThemeSchema(many=True, locale=locale) + dicostheme_schema = models.DicoSthemeSchema(many=True, locale=locale) + sites = site_schema.dump( - models.TSite.query.join(models.Observatory) - .filter( - models.TSite.publish_site == True, models.Observatory.is_published == True - ) - .order_by(dbconf["default_sort_sites"]) + getLocalizedSitesQuery().order_by(text(dbconf["default_sort_sites"])) ) for site in sites: cor_sthemes_themes = site.get("cor_site_stheme_themes") @@ -204,11 +225,11 @@ def getFiltersData(): filter for filter in filters if filter.get("name") == "township" ][0] str_map_in = ["'" + township + "'" for township in filter_township.get("items")] - sql_map_str = ( - "SELECT code_commune AS id, nom_commune AS label FROM geopaysages.communes WHERE code_commune IN (" - + ",".join(str_map_in) - + ")" - ) + sql_map_str = f"""SELECT c.code_commune AS id, ct.nom_commune AS label + FROM geopaysages.communes c + JOIN geopaysages.communes_translation ct on ct.row_id = c.code_commune + WHERE code_commune IN ({",".join(str_map_in)}) + AND ct.lang_id = '{locale}'""" sql_map = text(sql_map_str) townships_result = db.engine.execute(sql_map).fetchall() townships = [dict(row) for row in townships_result] @@ -291,7 +312,7 @@ def getItem(name, id): observatories.append( { "id": site["id_observatory"], - "label": site["observatory"]["title"], + "label": observatory["title"], "data": { "geom": observatory["geom"], "color": observatory["color"], From d4f2e993ab071ac55d97446828eb8db90738f61c Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Fri, 25 Oct 2024 14:53:42 +0200 Subject: [PATCH 031/107] fix: api get sites --- backend/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/api.py b/backend/api.py index 4a49281b..5408f159 100644 --- a/backend/api.py +++ b/backend/api.py @@ -8,6 +8,7 @@ current_app, ) from flask_login import login_required, current_user +from sqlalchemy import text from werkzeug.exceptions import NotFound from werkzeug.wsgi import FileWrapper @@ -219,7 +220,7 @@ def returnAllSites(): dbconf = utils.getDbConf() get_all_sites = ( models.TSite.query.join(models.TSiteTranslation) - .order_by(dbconf["default_sort_sites"]) + .order_by(text(dbconf["default_sort_sites"])) .all() ) sites = site_schema.dump(get_all_sites) From f3beef66c70243e12cca67db7b23ed6417d1e478 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Fri, 25 Oct 2024 15:00:44 +0200 Subject: [PATCH 032/107] refactor: rename api/languages to langs --- backend/api.py | 51 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/backend/api.py b/backend/api.py index 5408f159..ca5593f4 100644 --- a/backend/api.py +++ b/backend/api.py @@ -623,29 +623,28 @@ def returnAllcommunes(): return jsonify(communes), 200 -@api.route("/api/languages", methods=["GET"]) -def returnAllLanguages(): - get_all_languages = models.Lang.query.all() - languages = models.LangSchema(many=True).dump(get_all_languages) - return jsonify(languages), 200 +@api.route("/api/langs", methods=["GET"]) +def returnAllLangs(): + get_all_langs = models.Lang.query.all() + langs = models.LangSchema(many=True).dump(get_all_langs) + return jsonify(langs), 200 -@api.route("/api/languages", methods=["POST"]) +@api.route("/api/langs", methods=["POST"]) @fnauth.check_auth(6) -def add_languages(): - data = request.get_json() +def add_langs(): + lang = request.get_json() try: - get_all_existing_languages = models.Lang.query.all() - languages = {t.id: t for t in get_all_existing_languages} - for lang in data: - if lang["id"] not in languages: - lang_obj = models.Lang( - id=lang["id"], - label=lang["label"], - is_published=lang["is_published"], - is_default=lang["is_default"], - ) - db.session.add(lang_obj) + get_all_existing_langs = models.Lang.query.all() + langs = {t.id: t for t in get_all_existing_langs} + if lang["id"] not in langs: + lang_obj = models.Lang( + id=lang["id"], + label=lang["label"], + is_published=lang["is_published"], + is_default=lang["is_default"], + ) + db.session.add(lang_obj) db.session.commit() @@ -653,12 +652,12 @@ def add_languages(): db.session.rollback() return jsonify({"error": str(exception)}), 400 - return jsonify("languages added") + return jsonify("langs added") -@api.route("/api/language/", methods=["PATCH"]) +@api.route("/api/langs/", methods=["PATCH"]) @fnauth.check_auth(2) -def update_language(id): +def update_lang(id): data = request.get_json() try: models.Lang.query.filter_by(id=id).update(data) @@ -667,12 +666,12 @@ def update_language(id): db.session.rollback() return jsonify({"error": str(exception)}), 400 - return jsonify("language updated"), 200 + return jsonify("lang updated"), 200 -@api.route("/api/language/", methods=["DELETE"]) +@api.route("/api/langs/", methods=["DELETE"]) @fnauth.check_auth(6) -def delete_language(id): +def delete_lang(id): try: models.Lang.query.filter_by(id=id).delete() db.session.commit() @@ -680,7 +679,7 @@ def delete_language(id): db.session.rollback() return jsonify({"error": str(exception)}), 400 - return jsonify("language deleted"), 200 + return jsonify("lang deleted"), 200 @api.route("/api/logout", methods=["GET"]) From 6047207893900a52effb74549052c67259e8e045 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Fri, 25 Oct 2024 15:08:49 +0200 Subject: [PATCH 033/107] fix: post langs accept only one lang and serve 409 when needed --- backend/api.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/api.py b/backend/api.py index ca5593f4..75c4738f 100644 --- a/backend/api.py +++ b/backend/api.py @@ -637,14 +637,16 @@ def add_langs(): try: get_all_existing_langs = models.Lang.query.all() langs = {t.id: t for t in get_all_existing_langs} - if lang["id"] not in langs: - lang_obj = models.Lang( - id=lang["id"], - label=lang["label"], - is_published=lang["is_published"], - is_default=lang["is_default"], - ) - db.session.add(lang_obj) + if lang["id"] in langs: + return jsonify({"error": "lang already exists"}), 409 + + lang_obj = models.Lang( + id=lang["id"], + label=lang["label"], + is_published=lang["is_published"], + is_default=lang["is_default"], + ) + db.session.add(lang_obj) db.session.commit() From 1d84aace4c3682aa63641fad54595b13cf3dd89d Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Fri, 25 Oct 2024 16:10:39 +0200 Subject: [PATCH 034/107] fix: locale detection and redirection --- backend/routes.py | 33 ++++++++++++++++----------------- backend/utils.py | 9 +++++++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/backend/routes.py b/backend/routes.py index 6b56374e..64d2199e 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -23,27 +23,26 @@ def decorated_function(*args, **kwargs): locale = request.view_args.get("locale") if not utils.isMultiLangs() and locale is not None: return redirect(url_for(request.endpoint)) + langs = models.Lang.query.filter_by(is_published=True).all() + lang_ids = [lang.id for lang in langs] + defaultLang = next((lang for lang in langs if lang.is_default), None) + if utils.isMultiLangs() and locale not in lang_ids: + return redirect(url_for(request.endpoint, locale=defaultLang.id)) + if utils.isMultiLangs() and locale is None: - matched_locale = request.accept_languages.best_match(["fr", "en"]) - if matched_locale is None: - return redirect("/") - return redirect(url_for(request.endpoint, locale=matched_locale)) + userLang = request.accept_languages[0][0].split("-")[0] + if userLang in lang_ids: + return redirect(url_for(request.endpoint, locale=userLang)) + return redirect(url_for(request.endpoint, locale=defaultLang.id)) return f(*args, **kwargs) return decorated_function -def homeLocaleSelector(): - return "Select a language" - - @main.route("/") @main.route("//") +@localeGuard def home(locale=None): - if utils.isMultiLangs() and locale is None: - return homeLocaleSelector() - if not utils.isMultiLangs() and locale is not None: - return redirect("/") locale = utils.getLocale() site_schema = models.TSiteSchema(many=True, locale=locale) communes_schema = models.CommunesSchema(many=True, locale=locale) @@ -176,7 +175,7 @@ def home(locale=None): ) -@main.route("/gallery") +@main.route("/gallery/") def gallery(): data = utils.getFiltersData() @@ -188,8 +187,8 @@ def gallery(): ) -@main.route("/sites/") -@main.route("//sites/") +@main.route("/sites//") +@main.route("//sites//") @localeGuard def site(id_site, locale): site_schema = models.TSiteSchema(many=True, locale=locale) @@ -249,7 +248,7 @@ def getPhoto(photo): ) -@main.route("/sites//photos/latest") +@main.route("/sites//photos/latest/") def site_photos_last(id_site): site_schema = models.TSiteSchema(many=True) get_site_by_id = models.TSite.query.filter_by(id_site=id_site, publish_site=True) @@ -277,7 +276,7 @@ def site_photos_last(id_site): return render_template("site_photo.jinja", site=site, photo=photo) -@main.route("/sites") +@main.route("/sites/") @main.route("//sites/") @localeGuard def sites(locale=None): diff --git a/backend/utils.py b/backend/utils.py index 5e8a711f..f7c504b8 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -19,11 +19,16 @@ def getLocale(): - return request.view_args.get("locale", "fr") + locale = request.view_args.get("locale") + if locale is None: + lang = models.Lang.query.filter_by(is_default=True).first() + locale = lang.id + return locale def isMultiLangs(): - return False + count = models.Lang.query.filter_by(is_published=True).count() + return count > 1 def getCustomTpl(name): From a0b238b83edbc58e9ab2a4609fd226cff92dea2b Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Mon, 28 Oct 2024 18:46:47 +0100 Subject: [PATCH 035/107] fix: translations --- .../i18n/en/LC_MESSAGES/messages.mo | Bin 3623 -> 3711 bytes .../i18n/en/LC_MESSAGES/messages.po | 40 +++++++++++------- .../i18n/fr/LC_MESSAGES/messages.mo | Bin 3752 -> 3844 bytes .../i18n/fr/LC_MESSAGES/messages.po | 40 +++++++++++------- docker/custom.sample/i18n/messages.pot | 40 +++++++++++------- 5 files changed, 72 insertions(+), 48 deletions(-) mode change 100755 => 100644 docker/custom.sample/i18n/en/LC_MESSAGES/messages.po mode change 100755 => 100644 docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po diff --git a/docker/custom.sample/i18n/en/LC_MESSAGES/messages.mo b/docker/custom.sample/i18n/en/LC_MESSAGES/messages.mo index cfa351178136e7ecd2142cb94e5dd3b94514f87b..d821245eea1d2b3eb2f5c0ec5a78b3bac3e8ec4c 100644 GIT binary patch delta 1089 zcmYMzPe{{Y9LMpm`=iaxYSZb|Sw>0X!BJvr|DZo05p^gcAv!D?R%{tkgCH1m5j^Y? zb&n9MOT{{QFmH7!g6J@cxm%s?ET^Qg$CpGJl|*E=Xt*0M;Ei}*~+I>xJDE>m56-yr?t;65+V=x?;9sA&Ma{a*DmKJmGYf|? zji<2>FS~Q70~W9sS5UwEh$&pfHjFk0`?RCtJ8%<@d3+MvnNQ(1EMkW9t;#@^ETR&Y zu@&E766@}FRdES3IFH)@0jiKCkH5z>=i3hksw}c0upd?7 zepDr=P#Y9b7rKSrSoQcrRAnzvdGAnp-%uBB_Zt#YFQ|+CK_xa&7ii{P6i>KG)E`|ZYCVVAcNBH-q|eW}O{H2rJP^v{FU;JyZkG!A ti-p2 zkX%d*v@wxZhckeyt-glPDr?TJc%a@F2 zfH%c^TBhFlvmKP;JJ?3}pPMWCU?=sCCz|5x;r=0GrLSR#jnE%fMxf zV+LFCraOV!U>;j=1@+x~tjDi-5r1F|f1%b3D$N?O5j(IO6F7`Y{2s>G-yYDYBJddN zaLLUh8QQA%w@?f3y1%iLenq%cVGnA9A=CzUP>IiYd=ZuSOH?5nlKpMl2aZt{)m$ho z(1A**50g0T@iA0o4^ay~LCt%CO8A|Re?cX@hdR+es4HxUm{nsMvkTn!APuh7K64D5 zo^4?b7LjYRpB~>wasI#Q56;4zo?Unvnx%ziW%(3Zk$HVe~Q#>%Tdl>jXVKu_!?E& z8tNoIqVDPws\n" "Language: fr\n" @@ -18,23 +18,23 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.16.0\n" -#: utils.py:127 +#: utils.py:176 msgid "sites.filter.themes" msgstr "Topic" -#: utils.py:131 +#: utils.py:179 msgid "sites.filter.subthemes" msgstr "Sub-topic" -#: utils.py:136 +#: utils.py:185 msgid "sites.filter.township" msgstr "City" -#: utils.py:141 +#: utils.py:191 msgid "sites.filter.years" msgstr "Year" -#: utils.py:260 +#: utils.py:336 msgid "sites.filter.obervatories" msgstr "City" @@ -148,39 +148,39 @@ msgstr "Observation Sites" msgid "sites.observation_points.title" msgstr "Observation Sites" -#: tpl/sites.jinja:77 +#: tpl/sites.jinja:78 msgid "sites.observation_points.item" msgstr "observation site(s)" -#: tpl/sites.jinja:110 tpl/sites.jinja:113 +#: tpl/sites.jinja:112 tpl/sites.jinja:115 msgid "map.share.button" msgstr "Share" -#: tpl/sites.jinja:119 +#: tpl/sites.jinja:121 msgid "map.legend.obervatories" msgstr "city(ies)" -#: tpl/sites.jinja:125 +#: tpl/sites.jinja:127 msgid "map.legend.themes" msgstr "Theme(s)" -#: tpl/sites.jinja:137 +#: tpl/sites.jinja:139 msgid "map.legend.title" msgstr "Legend" -#: tpl/sites.jinja:147 +#: tpl/sites.jinja:149 msgid "map.share.dialog.title" msgstr "Share" -#: tpl/sites.jinja:148 +#: tpl/sites.jinja:150 msgid "map.share.dialog.message" msgstr "Copy and share the link above" -#: tpl/sites.jinja:165 +#: tpl/sites.jinja:167 msgid "map.share.copy_success.title" msgstr "Copied!" -#: tpl/sites.jinja:166 +#: tpl/sites.jinja:168 msgid "map.share.copy.copy_success.message" msgstr "The link is ready to paste." @@ -236,7 +236,7 @@ msgstr "1 month" msgid "comparatorv2.date.steps.1y" msgstr "1 year" -#: tpl/components/legal-footer.jinja:1 tpl/custom/footer.jinja:3 +#: tpl/components/legal-footer.jinja:1 msgid "footer.copyright" msgstr "© 2023 - GeoLandscapes, free and open-source software" @@ -253,9 +253,17 @@ msgid "header.nav.home" msgstr "Home" #: tpl/custom/main_menu.jinja:8 +msgid "header.nav.about" +msgstr "About" + +#: tpl/custom/main_menu.jinja:13 msgid "header.nav.sites" msgstr "Observation Sites" +#: tpl/custom/main_menu.jinja:18 +msgid "header.nav.gallery" +msgstr "Photo Gallery" + #~ msgid "home.map_block_title" #~ msgstr "Map of cities" diff --git a/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.mo b/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.mo index 1f9529057489252cd4fece1ede0ea9b3da2e2cc2..d7b985da3de3b19b35245957ff20bc669341e65b 100644 GIT binary patch delta 1093 zcmYMy-%Ha`7{~GBeq?J~rE|5M+Dau0T9}fvqNtnLO@;*tUM(6W%r=%ufwZVV5ZYaT zfk*}w(deeY1R=Vqo9IH62)ZePh~3nMR_{;W2m12eUgvqv&X4Du&9~p}sC;Y=+;=?3 z__Xnf_!Rwr*8E<69Xm+320RCG8}SIXV$qy2=gdXayers(zbuXho!jnQ#qFc9m5!4b z$FtauW9Bq!fjLa#BIHB$zy#~NDh*Zg05xC% zH{m;M!e#RtvSYVyalFx6aF3b6PWrQ`ipMd5GpPAzQH8v){0EG)zWYu?l?66>rcf0g zMO8A4njnwb&=l^#s^#ZVmAyiZdyg9T6}90t>u(8p8}31!@E}%nr{`&eaT2Qy&fP|? z+clA-SQkfKQ5!11-}1d?1{vaxBiHM)$TaRW>Lkygu4oiD;~4hi^)TnJi62|Xg8lF< zawu*ERoQ2Ye_H<^GaB(0PNEj*Mvd!3jXQ!V970`5$>Mv+>z+h7|3frh67^smH9?Zu zSi^Oq{u2lAUxmnNx~w^jyNO3FzKLDL(-uEMUiaMcOQBo|4z;ly>T1SO8=kOu3U?CUL0&h@mrmjtYW~+2ubK@%YRmp^Um|y*cx}R6 z&gU-X%jqk*$@HaMp^z`%sMUi{{f(Ja@4(@MeYJ<7v@f)DD^)5NOU0|TH{nmdzn__B A5C8xG delta 1033 zcmYMyPe>GD7{~F)UDrRmlcsK|rM7NKh7h5p$H|^ zIwnyhfu}iCDAX-H2wDdhvO1WD4tvtVO3?3*eZXO7Kl3~@^SsadzVo1Ep*i=xwsg)o zdN`Xn_e&HV9h=c&{u{QC{^vH9nw=!>##4CB9d{?)c~spcOk&>S0@j=5EFLpE&W$vl zz$`XnzdMQ=U=mN`JnFe+ti{iG47afwcTn}BWoC7l#8zy_1{^>&ejBS9-zK<-lX!$R zIOo1bI<)s5ucHcYxx09lI96Vq;RVzHy{G|(QH|gA{1a5;Z%`BYC>h^2{l*?@MpYHX z0*j-)I=ZAm>=VHd*75fyh{`zRXkkE{_ zkX5pR`v=p+2Og*C<{WVc>bdKv28TR9irSLLsFiqus=MgC2pby9{2dZI~)Fuwnf6LWiO-U*;HrdayAndDlbN6 Qz9yPx?gSO#V6YfD1OUKa`2YX_ diff --git a/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po b/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po old mode 100755 new mode 100644 index 1ad0b7e5..24a1cadd --- a/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po +++ b/docker/custom.sample/i18n/fr/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-23 12:32+0000\n" +"POT-Creation-Date: 2024-10-28 17:23+0000\n" "PO-Revision-Date: 2018-12-21 11:34+0100\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -18,23 +18,23 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.16.0\n" -#: utils.py:127 +#: utils.py:176 msgid "sites.filter.themes" msgstr "Thème" -#: utils.py:131 +#: utils.py:179 msgid "sites.filter.subthemes" msgstr "Sous-thème" -#: utils.py:136 +#: utils.py:185 msgid "sites.filter.township" msgstr "Commune" -#: utils.py:141 +#: utils.py:191 msgid "sites.filter.years" msgstr "Année" -#: utils.py:260 +#: utils.py:336 msgid "sites.filter.obervatories" msgstr "Observatoire" @@ -148,39 +148,39 @@ msgstr "Sites d'observation" msgid "sites.observation_points.title" msgstr "Sites d'observation" -#: tpl/sites.jinja:77 +#: tpl/sites.jinja:78 msgid "sites.observation_points.item" msgstr "site(s) d'observation" -#: tpl/sites.jinja:110 tpl/sites.jinja:113 +#: tpl/sites.jinja:112 tpl/sites.jinja:115 msgid "map.share.button" msgstr "Partager" -#: tpl/sites.jinja:119 +#: tpl/sites.jinja:121 msgid "map.legend.obervatories" msgstr "Observatoire(s)" -#: tpl/sites.jinja:125 +#: tpl/sites.jinja:127 msgid "map.legend.themes" msgstr "Thème(s)" -#: tpl/sites.jinja:137 +#: tpl/sites.jinja:139 msgid "map.legend.title" msgstr "Légende" -#: tpl/sites.jinja:147 +#: tpl/sites.jinja:149 msgid "map.share.dialog.title" msgstr "Partager" -#: tpl/sites.jinja:148 +#: tpl/sites.jinja:150 msgid "map.share.dialog.message" msgstr "Copier et partager le lien ci-dessus" -#: tpl/sites.jinja:165 +#: tpl/sites.jinja:167 msgid "map.share.copy_success.title" msgstr "Copié !" -#: tpl/sites.jinja:166 +#: tpl/sites.jinja:168 msgid "map.share.copy.copy_success.message" msgstr "Le lien est prêt à être coller." @@ -236,7 +236,7 @@ msgstr "1 mois" msgid "comparatorv2.date.steps.1y" msgstr "1 an" -#: tpl/components/legal-footer.jinja:1 tpl/custom/footer.jinja:3 +#: tpl/components/legal-footer.jinja:1 msgid "footer.copyright" msgstr "© 2023 - GeoPaysages, logiciel libre et open-source" @@ -253,9 +253,17 @@ msgid "header.nav.home" msgstr "Accueil" #: tpl/custom/main_menu.jinja:8 +msgid "header.nav.about" +msgstr "À propos" + +#: tpl/custom/main_menu.jinja:13 msgid "header.nav.sites" msgstr "Sites d'observation" +#: tpl/custom/main_menu.jinja:18 +msgid "header.nav.gallery" +msgstr "Galerie photo" + #~ msgid "map.filter.themes" #~ msgstr "Thème" diff --git a/docker/custom.sample/i18n/messages.pot b/docker/custom.sample/i18n/messages.pot index 52d31162..72fe30b6 100644 --- a/docker/custom.sample/i18n/messages.pot +++ b/docker/custom.sample/i18n/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-23 12:32+0000\n" +"POT-Creation-Date: 2024-10-28 17:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,23 +17,23 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.16.0\n" -#: utils.py:127 +#: utils.py:176 msgid "sites.filter.themes" msgstr "" -#: utils.py:131 +#: utils.py:179 msgid "sites.filter.subthemes" msgstr "" -#: utils.py:136 +#: utils.py:185 msgid "sites.filter.township" msgstr "" -#: utils.py:141 +#: utils.py:191 msgid "sites.filter.years" msgstr "" -#: utils.py:260 +#: utils.py:336 msgid "sites.filter.obervatories" msgstr "" @@ -147,39 +147,39 @@ msgstr "" msgid "sites.observation_points.title" msgstr "" -#: tpl/sites.jinja:77 +#: tpl/sites.jinja:78 msgid "sites.observation_points.item" msgstr "" -#: tpl/sites.jinja:110 tpl/sites.jinja:113 +#: tpl/sites.jinja:112 tpl/sites.jinja:115 msgid "map.share.button" msgstr "" -#: tpl/sites.jinja:119 +#: tpl/sites.jinja:121 msgid "map.legend.obervatories" msgstr "" -#: tpl/sites.jinja:125 +#: tpl/sites.jinja:127 msgid "map.legend.themes" msgstr "" -#: tpl/sites.jinja:137 +#: tpl/sites.jinja:139 msgid "map.legend.title" msgstr "" -#: tpl/sites.jinja:147 +#: tpl/sites.jinja:149 msgid "map.share.dialog.title" msgstr "" -#: tpl/sites.jinja:148 +#: tpl/sites.jinja:150 msgid "map.share.dialog.message" msgstr "" -#: tpl/sites.jinja:165 +#: tpl/sites.jinja:167 msgid "map.share.copy_success.title" msgstr "" -#: tpl/sites.jinja:166 +#: tpl/sites.jinja:168 msgid "map.share.copy.copy_success.message" msgstr "" @@ -235,7 +235,7 @@ msgstr "" msgid "comparatorv2.date.steps.1y" msgstr "" -#: tpl/components/legal-footer.jinja:1 tpl/custom/footer.jinja:3 +#: tpl/components/legal-footer.jinja:1 msgid "footer.copyright" msgstr "" @@ -252,6 +252,14 @@ msgid "header.nav.home" msgstr "" #: tpl/custom/main_menu.jinja:8 +msgid "header.nav.about" +msgstr "" + +#: tpl/custom/main_menu.jinja:13 msgid "header.nav.sites" msgstr "" +#: tpl/custom/main_menu.jinja:18 +msgid "header.nav.gallery" +msgstr "" + From 7bde4de0b35b4a79eaa6f5dab31de531688fa759 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Mon, 28 Oct 2024 18:57:59 +0100 Subject: [PATCH 036/107] style: code format --- backend/app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/app.py b/backend/app.py index ff3def93..29c8b43f 100755 --- a/backend/app.py +++ b/backend/app.py @@ -56,10 +56,12 @@ def __call__(self, environ, start_response): # app.wsgi_app = ReverseProxied(app.wsgi_app) CORS(app, supports_credentials=True) + @babel.localeselector def determine_locale(): return utils.getLocale() + app.register_blueprint(main_blueprint) app.register_blueprint(api) app.register_blueprint(custom_app.custom) @@ -75,9 +77,9 @@ def determine_locale(): def inject_to_tpl(): custom = custom_app.custom_inject_to_tpl() data = dict( - dbconf=utils.getDbConf(), - debug=app.debug, - locale=utils.getLocale(), + dbconf=utils.getDbConf(), + debug=app.debug, + locale=utils.getLocale(), isMultiObservatories=utils.isMultiObservatories, getThumborUrl=utils.getThumborUrl, getCustomTpl=utils.getCustomTpl, From e8d0eea074111f961e80b3e7ca5ff3849de2024a Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Mon, 28 Oct 2024 19:04:14 +0100 Subject: [PATCH 037/107] fix: localize gallery route --- backend/routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/routes.py b/backend/routes.py index 64d2199e..c6ec0c5c 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -176,7 +176,8 @@ def home(locale=None): @main.route("/gallery/") -def gallery(): +@main.route("//gallery/") +def gallery(locale): data = utils.getFiltersData() return render_template( From 9aee3617a41ecb761ffef53ad04160df88bbf717 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Tue, 29 Oct 2024 14:39:21 +0100 Subject: [PATCH 038/107] fix: model schema fallback to default translation --- backend/models.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/models.py b/backend/models.py index e38ffcfa..747a697e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -383,10 +383,25 @@ def _deserialize(self, value, attr, data): def get_translated_data(self, data): if not self.lang_id: return data + + translation = None + for data_translation in data["translations"]: + if data_translation["lang_id"] == self.lang_id: + translation = data_translation + break + + if not translation: + translation = next( + ( + translation + for translation in data["translations"] + if translation["lang"]["is_default"] == True + ), + None, + ) + for field in self.translatable_fields: - for translation in data["translations"]: - if translation["lang_id"] == self.lang_id: - data[field] = translation[field] + data[field] = translation[field] return data From 1e7906811db50e76f0a5916cdf0c5c8b0ecd5bfc Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Tue, 29 Oct 2024 14:41:08 +0100 Subject: [PATCH 039/107] fix: redirection and other bug about locale --- backend/routes.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/routes.py b/backend/routes.py index c6ec0c5c..1222d3d3 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -26,14 +26,22 @@ def decorated_function(*args, **kwargs): langs = models.Lang.query.filter_by(is_published=True).all() lang_ids = [lang.id for lang in langs] defaultLang = next((lang for lang in langs if lang.is_default), None) - if utils.isMultiLangs() and locale not in lang_ids: - return redirect(url_for(request.endpoint, locale=defaultLang.id)) + if utils.isMultiLangs() and locale is not None and locale not in lang_ids: + view_args = dict(**request.view_args) + view_args.pop("locale", None) + return redirect( + url_for(request.endpoint, locale=defaultLang.id, **view_args) + ) if utils.isMultiLangs() and locale is None: userLang = request.accept_languages[0][0].split("-")[0] if userLang in lang_ids: - return redirect(url_for(request.endpoint, locale=userLang)) - return redirect(url_for(request.endpoint, locale=defaultLang.id)) + return redirect( + url_for(request.endpoint, locale=userLang, **request.view_args) + ) + return redirect( + url_for(request.endpoint, locale=defaultLang.id, **request.view_args) + ) return f(*args, **kwargs) return decorated_function @@ -194,7 +202,9 @@ def gallery(locale): def site(id_site, locale): site_schema = models.TSiteSchema(many=True, locale=locale) communes_schema = models.CommunesSchema(many=True, locale=locale) - get_site_by_id = models.TSite.query.filter_by(id_site=id_site, publish_site=True) + get_site_by_id = utils.getLocalizedSitesQuery().filter( + models.TSite.id_site == id_site + ) site = site_schema.dump(get_site_by_id) if len(site) == 0: return abort(404) @@ -294,7 +304,7 @@ def sites(locale=None): @main.route("/legal-notices/") @main.route("//legal-notices/") @localeGuard -def legal_notices(): +def legal_notices(locale): tpl = utils.getCustomTpl("legal_notices") From f6180470b8c04f5ae060d05c3afa72cfcceb6ec4 Mon Sep 17 00:00:00 2001 From: Vincent Bourgeois Date: Tue, 29 Oct 2024 14:41:59 +0100 Subject: [PATCH 040/107] fix: render localized links in tpl --- backend/app.py | 1 + backend/tpl/components/legal-footer.jinja | 2 +- backend/tpl/gallery.jinja | 4 ++-- backend/tpl/layout.jinja | 6 +++--- backend/utils.py | 6 ++++++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/app.py b/backend/app.py index 29c8b43f..346b4607 100755 --- a/backend/app.py +++ b/backend/app.py @@ -83,6 +83,7 @@ def inject_to_tpl(): isMultiObservatories=utils.isMultiObservatories, getThumborUrl=utils.getThumborUrl, getCustomTpl=utils.getCustomTpl, + getLocalizedLink=utils.getLocalizedLink, ) data.update(custom) return data diff --git a/backend/tpl/components/legal-footer.jinja b/backend/tpl/components/legal-footer.jinja index 87ec3b05..fb5edb46 100644 --- a/backend/tpl/components/legal-footer.jinja +++ b/backend/tpl/components/legal-footer.jinja @@ -2,5 +2,5 @@ {% if getCustomTpl('legal_notices') %} - - Mentions Légales + Mentions Légales {% endif %} \ No newline at end of file diff --git a/backend/tpl/gallery.jinja b/backend/tpl/gallery.jinja index e2c26e3a..2d0d7191 100644 --- a/backend/tpl/gallery.jinja +++ b/backend/tpl/gallery.jinja @@ -48,14 +48,14 @@ @mouseenter="onSiteMousover(site)" @mouseleave="onSiteMouseout(site)" > - +
- + diff --git a/backend/tpl/layout.jinja b/backend/tpl/layout.jinja index 84c98118..26746d60 100644 --- a/backend/tpl/layout.jinja +++ b/backend/tpl/layout.jinja @@ -72,17 +72,17 @@
{{ _('footer.internal_title') }}