From ea9e85687351fc8db78cd70a0089100929bc23d9 Mon Sep 17 00:00:00 2001 From: Jules Jean-Louis Date: Wed, 23 Oct 2024 12:04:47 +0200 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 07/12] 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 08/12] 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 09/12] 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 10/12] 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 11/12] 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 12/12] 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()