Flask ihome : Affichage des Annonces et Détails des Logements

Lorsqu'un utilisateur accède à la section "Mes Logements" de l'application ihome, il doit y voir une liste complète des annonces qu'il a précédemment publiées.

Logique Backend pour la Liste des Annonces

Pour cette page, le serveur doit fournir des informations clés pour chaque annonce : li'dentifiant du logement, son titre, la zone géographique, le prix par nuit, la date de publication et l'URL de l'image par défaut. Plutôt que de récupérer chaque champ individuellement dans la vue, il est plus propre d'intégrer une méthode au sein du modèle de données Logement (anciennement Houses) qui centralise la collecte de ces informations.

Mise à jour du Modèle de Logement (ihome/models.py)

# ihome/models.py
from datetime import datetime
from . import db # Assurez-vous que la base de données est importée si nécessaire

# Supposons que BaseModel soit la classe de base de vos modèles
class BaseModel:
    # ... (implémentation de BaseModel, ex: id, created_date, updated_date)
    pass

class Logement(BaseModel):
    """Modèle représentant un logement mis en location."""
    __tablename__ = 'ih_logements'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(64), nullable=False)
    price = db.Column(db.Integer, default=0) # Prix par nuit
    default_image_url = db.Column(db.String(256)) # Image par défaut
    area_id = db.Column(db.Integer, db.ForeignKey('ih_zones.id'), nullable=False)
    area = db.relationship('Zone') # Relation avec le modèle Zone
    created_date = db.Column(db.DateTime, default=datetime.now)
    # ... autres attributs de logement ...

    def obtenir_informations_pour_liste(self):
        """
        Retourne un dictionnaire contenant les informations essentielles
        d'un logement destinées à l'affichage dans une liste.
        """
        return dict(
            id_annonce=self.id,
            titre_annonce=self.title,
            nom_zone=self.area.name,
            prix_nuit=self.price,
            date_publication=datetime.strftime(self.created_date, '%Y-%m-%d %H:%M:%S'),
            url_image_principale=self.default_image_url,
        )

class Zone(BaseModel): # Modèle Zone pour les relations
    __tablename__ = 'ih_zones'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), unique=True, nullable=False)
    # ...

Notez que la date de création, stockée comme un objet datetime, est convertie en une chaîne de caractères formatée pour une meilleure lisibilité côté front-end.

Création de l'Endpoint API (ihome/api_1_0/logements.py)

Un nouvel endpoint est nécessaier pour exposer cette liste d'annonces. Il sera protégé par une vérification de connexion.

# ihome/api_1_0/logements.py
from flask import jsonify, g, current_app, session, Blueprint
from ihome.utils.response_code import RET # Code d'erreur/succès
from ihome import db # Instance de la base de données
from ihome.models import Logement, Utilisateur # Modèles de Logement et Utilisateur
from ihome.utils.decorators import connexion_requise # Décorateur de connexion

api = Blueprint('api_1_0', __name__) # Assurez-vous que api est défini

# Supposons que le modèle Utilisateur existe avec une relation 'logements'
class Utilisateur(BaseModel):
    __tablename__ = 'ih_utilisateurs'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), unique=True, nullable=False)
    # ... autres attributs utilisateur
    logements = db.relationship('Logement', backref='utilisateur', lazy='dynamic')


@api.route('/utilisateur/annonces')
@connexion_requise # Exige que l'utilisateur soit connecté
def recuperer_annonces_utilisateur():
    """
    Renvoie la liste des annonces publiées par l'utilisateur connecté.
    """
    utilisateur_connecte = g.utilisateur # L'utilisateur est stocké dans g par le décorateur
    
    try:
        # Récupère tous les objets Logement associés à cet utilisateur
        liste_logements = utilisateur_connecte.logements.all()
    except Exception as e:
        current_app.logger.error(f"Erreur DB lors de la récupération des annonces de l'utilisateur: {e}")
        return jsonify(code_erreur=RET.DBERR, message_erreur="Échec de l'obtention des informations sur les annonces")
    
    # Formate chaque objet logement en un dictionnaire pour le front-end
    donnees_formattees = [logement_obj.obtenir_informations_pour_liste() for logement_obj in liste_logements]
    
    return jsonify(code_erreur=RET.OK, donnees=donnees_formattees)


Logique Frontend pour la Liste des Annonces

Le backend retourne une liste de dictionnaires JSON. Le front-end doit itérer sur cette liste pour afficher les détails de chaque annonce. L'utilisation d'un moteur de templating comme art-template est idéale pour cette tâche.

Modification du Fichier HTML (myhouse.html)

Insérez le modèle art-template là où la liste des annonces doit être rendue.

<ul id="liste-annonces-utilisateur" class="liste-annonces">
    <li>
        <div class="nouvelle-annonce">
            <a href="/publier_annonce.html">Publier une nouvelle annonce</a>
        </div>
    </li>
    <script type="text/html" id="modele-item-logement">
        {{ each annonces as annonce_courante }}
            <li>
                <a href="/detail_logement.html?id={{annonce_courante.id_annonce}}">
                    <div class="entete-annonce">
                        <h3>ID Annonce: {{ annonce_courante.id_annonce }} — {{ annonce_courante.titre_annonce }}</h3>
                    </div>
                    <div class="corps-annonce">
                        <img src="{{ annonce_courante.url_image_principale }}" alt="Image du logement">
                        <div class="texte-description-annonce">
                            <ul>
                                <li>Localisation: {{ annonce_courante.nom_zone }}</li>
                                <li>Prix: {{ annonce_courante.prix_nuit }} €/nuit</li>
                                <li>Publié le: {{ annonce_courante.date_publication }}</li>
                            </ul>
                        </div>
                    </div>
                </a>
            </li>
        {{ /each }}
    </script>
</ul>

Le bloc <script type="text/html" id="..."> délimite le modèle. La syntaxe {{ each ... }} permet d'itérer sur la collection annonces, chaque élément étant accessible via la variable annonce_courante.

Mise à jour du Fichier JavaScript (myhouse.js)

Le code JavaScript effectue l'appel API et utilise art-template pour rendre le contenu.

// Envoi d'une requête AJAX pour récupérer les annonces de l'utilisateur
$.get('/api/v1.0/utilisateur/annonces', function (reponseApi) {
    if (reponseApi.code_erreur === '0'){
        // Utilise le modèle pour générer le HTML avec les données reçues
        var htmlGenerer = template('modele-item-logement', {annonces: reponseApi.donnees});
        // Ajoute le HTML généré à l'élément de liste d'annonces
        $('#liste-annonces-utilisateur').append(htmlGenerer);
    } else {
        alert('Erreur: ' + reponseApi.message_erreur);
    }
}, 'json');

La fonction template() prend en premier paramètre l'ID du modèle HTML et en second un objet JavaScript dont les clés correspondent aux variables utilisées dans le modèle (ici, annonces). Elle retourne une chaîne HTML qui est ensuite insérée dans le DOM.


Page de Détails des Logements

Cliquer sur une annonce dans la liste mène à sa page de détails, typiquement via une URL comme /detail_logement.html?id=ID_LOGEMENT.

Logique Backend pour la Page de Détails

Similaire à la liste, un grand nombre d'informations est requis pour la page de détails. Une méthode centralisée dans le modèle Logement est préférable.

Mise à jour des Modèles (ihome/models.py)

# ihome/models.py (suite)
# ... dans la classe Logement ...
    images = db.relationship('ImageLogement', backref='logement', lazy='dynamic') # Relation Images
    equipements = db.relationship('Equipement', secondary='ih_logement_equipement', lazy='dynamic') # Relation Equipements
    # ... autres attributs ...

    def obtenir_details_complets(self):
        """Récupère l'ensemble des informations détaillées d'un logement."""
        return dict(
            urls_toutes_images=[img.url for img in self.images],
            titre=self.title,
            tarif_nuit=self.price,
            id_proprietaire=self.utilisateur.id,
            url_img_proprietaire=self.utilisateur.image_url, # Supposons un attribut image_url sur Utilisateur
            nom_proprietaire=self.utilisateur.name,
            adresse_complete=self.address,
            nombre_chambres=self.room_count,
            superficie_m2=self.acreage,
            type_logement=self.unit,
            capacite_accueil=self.capacity,
            description_lits=self.beds,
            depot_garantie=self.deposit,
            jours_min_sejour=self.min_days,
            jours_max_sejour=self.max_days,
            ids_equipements=[eq.id for eq in self.equipements],
            commentaires_clients=[cmd.generer_dictionnaire_commentaire() for cmd in self.recuperer_commandes_avec_commentaires()],
        )
    
    def recuperer_commandes_avec_commentaires(self):
        """
        Récupère les commandes complétées et commentées associées à ce logement.
        Limité aux N derniers commentaires pour l'affichage.
        """
        # Supposons COMMENTAIRES_A_AFFICHER est une constante définie (ex: 10)
        from ihome.constants import COMMENTAIRES_A_AFFICHER 
        commandes_commentees = Commande.query.filter(
            Commande.id_logement == self.id,
            Commande.statut == 'COMPLETED',
            Commande.texte_commentaire.isnot(None)
        ).order_by(Commande.date_mise_a_jour.desc()).limit(COMMENTAIRES_A_AFFICHER).all()
        return commandes_commentees

# ... Autres modèles (ImageLogement, Equipement)
class ImageLogement(BaseModel):
    __tablename__ = 'ih_images_logement'
    id = db.Column(db.Integer, primary_key=True)
    logement_id = db.Column(db.Integer, db.ForeignKey('ih_logements.id'), nullable=False)
    url = db.Column(db.String(256), nullable=False)

class Equipement(BaseModel):
    __tablename__ = 'ih_equipements'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), nullable=False)

# ... Modèle Commande (Orders)
class Commande(BaseModel):
    """Modèle représentant une commande de logement."""
    __tablename__ = 'ih_commandes'
    id = db.Column(db.Integer, primary_key=True)
    id_logement = db.Column(db.Integer, db.ForeignKey('ih_logements.id'), nullable=False)
    id_utilisateur = db.Column(db.Integer, db.ForeignKey('ih_utilisateurs.id'), nullable=False)
    statut = db.Column(db.String(32), default="NEW", index=True) # Ex: NEW, PENDING, COMPLETED, CANCELLED
    texte_commentaire = db.Column(db.Text) # Texte du commentaire
    date_mise_a_jour = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
    utilisateur = db.relationship('Utilisateur') # Relation avec l'utilisateur

    def generer_dictionnaire_commentaire(self):
        """Prépare les détails d'un commentaire pour l'affichage."""
        nom_affichable = self.utilisateur.name if self.utilisateur.name and self.utilisateur.name != self.utilisateur.phone else 'Utilisateur Anonyme'
        return dict(
            nom_client=nom_affichable,
            date_commentaire=datetime.strftime(self.date_mise_a_jour, '%Y-%m-%d %H:%M:%S'),
            contenu_commentaire=self.texte_commentaire
        )


Les propriétés telles que les images, les équipements et les commentaires sont gérées comme des listes grâce aux relations SQLAlchemy, transformées en listes d'IDs ou de dictionnaires.

Implémentation de l'Endpoint de Détails (ihome/api_1_0/logements.py)

L'endpoint pour les détails utilise Redis pour la mise en cache afin d'améliorer les performances, étant donné que les détails des logements sont fréquemment consultés mais rarement modifiés.

# ihome/api_1_0/logements.py (suite)
import json
from ihome import redis_conn # Connexion à Redis
from ihome.constants import DUREE_CACHE_DETAILS_LOGEMENT # Constante pour la durée d'expiration du cache

@api.route('/logements/<id_logement>')
def obtenir_details_logement(id_logement):
    """
    Retourne les informations détaillées d'un logement, avec mise en cache Redis.
    """
    id_utilisateur_actuel = session.get('id_utilisateur', '-1') # -1 si non connecté
    
    cle_cache_redis = f'logement_details_{id_logement}'
    details_logement_json = None

    try:
        # Tente de récupérer les données du cache Redis
        donnees_cachees = redis_conn.get(cle_cache_redis)
        if donnees_cachees:
            details_logement_json = donnees_cachees.decode('utf-8')
    except Exception as e:
        current_app.logger.error(f"Erreur de lecture Redis pour le logement {id_logement}: {e}")
    
    # Si les données ne sont pas dans le cache, les charger depuis la DB
    if not details_logement_json:
        try:
            objet_logement = Logement.query.get(id_logement)
        except Exception as e:
            current_app.logger.error(f"Erreur DB lors de l'accès au logement {id_logement}: {e}")
            return jsonify(code_erreur=RET.DBERR, message_erreur="Impossible de récupérer les informations du logement")
        
        if not objet_logement:
            return jsonify(code_erreur=RET.NODATA, message_erreur="Logement introuvable")
        
        # Récupère les détails formatés
        details_complets = objet_logement.obtenir_details_complets()
        details_logement_json = json.dumps(details_complets)
        
        # Stocke les données dans le cache Redis
        try:
            redis_conn.setex(cle_cache_redis, DUREE_CACHE_DETAILS_LOGEMENT, details_logement_json)
        except Exception as e:
            current_app.logger.error(f"Erreur d'écriture Redis pour le logement {id_logement}: {e}")

    # Construit la réponse finale
    reponse_payload = dict(
        code_erreur=RET.OK,
        donnees=dict(
            id_utilisateur=id_utilisateur_actuel,
            logement=json.loads(details_logement_json) # Convertit la chaîne JSON en objet Python
        )
    )
    return jsonify(reponse_payload)
</id_logement>

L'ID de l'utilisateur connecté est renvoyé avec les détails du logement pour permettre au front-end de déterminer si un bouton de réservation doit être affiché (si l'utilisateur n'est pas le propriétaire de l'annonce).

Logique Frontend pour la Page de Détails

La page de détails affiche une multitude d'informations, ce qui rend l'utilisation d'un moteur de templating encore plus pertinente.

Modification du Fichier HTML (detail_logement.html)

Intégrez les modèles art-template pour les images et les informations détaillées du logement.

<div class="conteneur-carrousel-images">
    <script type="text/html" id="modele-images-logement">
        <div class="swiper-pagination"></div>
        <ul class="swiper-wrapper">
            {{ each urls_images as url_img_unique }}
            <li class="swiper-slide"><img src="{{ url_img_unique }}" alt="Image du logement"></li>
            {{ /each }}
        </ul>
        <div class="affichage-prix-logement"><span>{{ prix_par_nuit }}</span> €/nuit</div>
    </script>
</div>
<!-- ... autres éléments de la page ... -->
<div class="zone-details-principale">
    <script type="text/html" id="modele-infos-details-logement">
        <div class="entete-details-section mise-en-page">
            <h2 class="titre-logement">{{ logement_data.titre }}</h2>
            <div class="photo-proprietaire"><img src="{{ logement_data.url_img_proprietaire }}" alt="Photo du propriétaire"></div>
            <h2 class="nom-proprietaire">Propriétaire: <span>{{ logement_data.nom_proprietaire }}</span></h2>
        </div>
        <div class="bloc-infos-logement mise-en-page">
           <h3>Adresse</h3>
           <ul class="liste-infos-details texte-centre">
                <li>{{ logement_data.adresse_complete }}</li>
           </ul>
        </div>
        <ul class="type-chambre-details mise-en-page">
            <li>
                <span class="icone-batiment"></span>
                <div class="texte-icone">
                    <h3>Propose {{ logement_data.nombre_chambres }} chambre(s)</h3>
                    <p>Superficie: {{ logement_data.superficie_m2 }} m²</p>
                    <p>Type de logement: {{ logement_data.type_logement }}</p>
                </div>
            </li>
            <li>
                <span class="icone-personnes"></span>
                <div class="texte-icone">
                    <h3>Capacité d'accueil: {{ logement_data.capacite_accueil }} personne(s)</h3>
                </div>
            </li>
            <li>
                <span class="icone-lit"></span>
                <div class="texte-icone">
                    <h3>Disposition des couchages</h3>
                    <p>{{ logement_data.description_lits }}</p>
                </div>
            </li>
        </ul>
        <div class="bloc-infos-logement mise-en-page">
            <h3>Détails de la réservation</h3>
            <ul class="liste-infos-details">
                <li>Caution: <span>{{ logement_data.depot_garantie }} €</span></li>
                <li>Séjour minimum: <span>{{ logement_data.jours_min_sejour }} jours</span></li>
                <li>Séjour maximum: <span>{{ if logement_data.jours_max_sejour >= 0 }}{{ logement_data.jours_max_sejour }} jours{{ else }}Illimité{{ /if }}</span></li>
            </ul>
        </div>
        <div class="bloc-equipements-logement mise-en-page">
            <h3>Équipements</h3>
            <ul class="liste-equipements-details clearfix">
                <li><span class="{{ if logement_data.ids_equipements.indexOf(1) >= 0 }}icone-wifi {{ else }}icone-non-disponible{{ /if }}"></span>Wi-Fi</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(2) >= 0 }}icone-douche {{ else }}icone-non-disponible{{ /if }}"></span>Douche chaude</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(3) >= 0 }}icone-climatisation {{ else }}icone-non-disponible{{ /if }}"></span>Climatisation</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(4) >= 0 }}icone-television {{ else }}icone-non-disponible{{ /if }}"></span>Télévision</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(5) >= 0 }}icone-machine-laver {{ else }}icone-non-disponible{{ /if }}"></span>Lave-linge</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(6) >= 0 }}icone-refrigerateur {{ else }}icone-non-disponible{{ /if }}"></span>Réfrigérateur</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(7) >= 0 }}icone-chauffage {{ else }}icone-non-disponible{{ /if }}"></span>Chauffage</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(8) >= 0 }}icone-cuisine {{ else }}icone-non-disponible{{ /if }}"></span>Cuisine</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(9) >= 0 }}icone-micro-ondes {{ else }}icone-non-disponible{{ /if }}"></span>Micro-ondes</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(10) >= 0 }}icone-seche-cheveux {{ else }}icone-non-disponible{{ /if }}"></span>Sèche-cheveux</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(11) >= 0 }}icone-ascenseur {{ else }}icone-non-disponible{{ /if }}"></span>Ascenseur</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(12) >= 0 }}icone-parking {{ else }}icone-non-disponible{{ /if }}"></span>Parking</li>
                <li><span class="{{ if logement_data.ids_equipements.indexOf(13) >= 0 }}icone-gardien {{ else }}icone-non-disponible{{ /if }}"></span>Gardien</li>
            </ul>
        </div>
        {{ if logement_data.commentaires_clients.length > 0 }}
        <div class="bloc-infos-logement mise-en-page">
            <h3>Avis des clients</h3>
            <ul class="liste-avis-logement">
                {{ each logement_data.commentaires_clients as commentaire_client }}
                <li>
                    <p>Client: {{ commentaire_client.nom_client }}<span class="fr">{{ commentaire_client.date_commentaire }}</span></p>
                    <p>Avis: {{ commentaire_client.contenu_commentaire }}</p>
                </li>
                {{ /each }}
            </ul>
        </div>
        {{ /if }}
    </script>
</div>

Mise à jour du Fichier JavaScript (detail_logement.js)

// Fonction utilitaire pour décoder les paramètres d'URL
function decodageParametresURL() {
    var params = {};
    window.location.search.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(st, key, value) {
        params[key] = decodeURIComponent(value);
    });
    return params;
}

$(document).ready(function(){
    var parametresUrl = decodageParametresURL();
    var logementId = parametresUrl ? parametresUrl.id : null;

    if (logementId) {
        $.get('/api/v1.0/logements/' + logementId, function (reponseApi) {
            if (reponseApi.code_erreur === '0') {
                var donneesDetail = reponseApi.donnees;
                
                // Remplir le carrousel d'images avec art-template
                $('.conteneur-carrousel-images').html(template('modele-images-logement', {
                    urls_images: donneesDetail.logement.urls_toutes_images,
                    prix_par_nuit: donneesDetail.logement.tarif_nuit
                }));
                
                // Afficher les autres détails du logement
                $('.zone-details-principale').html(template('modele-infos-details-logement', {
                    logement_data: donneesDetail.logement
                }));
                
                // Logique pour afficher ou cacher le bouton de réservation
                var boutonReservation = $(".bouton-reserver-maintenant");
                if (donneesDetail.id_utilisateur !== donneesDetail.logement.id_proprietaire) {
                    boutonReservation.show();
                } else {
                    boutonReservation.hide(); // S'assurer qu'il est caché si c'est le propriétaire
                }
                
                // Définir l'URL du bouton de réservation
                var urlCibleReservation;
                if (donneesDetail.id_utilisateur !== '-1'){
                    // Utilisateur connecté: redirection vers la page de réservation
                    urlCibleReservation = '/page_reservation.html?id=' + logementId;
                } else {
                    // Non connecté: redirection vers la page de connexion
                    urlCibleReservation = '/page_connexion.html';
                }
                boutonReservation.attr('href', urlCibleReservation);

                // Initialisation du carrousel Swiper.
                // IMPORTANT: Ceci doit être exécuté APRÈS que le HTML du carrousel ait été généré par le template.
                new Swiper ('.conteneur-carrousel-images', {
                    loop: true,
                    autoplay: {
                        delay: 2000,
                        disableOnInteraction: false,
                    },
                    pagination: {
                        el: '.swiper-pagination',
                        type: 'fraction',
                    },
                });
            } else {
                alert('Erreur lors du chargement des détails du logement: ' + reponseApi.message_erreur);
            }
        });
    } else {
        alert('ID de logement manquant dans l\'URL.');
    }
});

L'intégration du carrousel Swiper est cruciale. Son initialisation doit impérativement avoir lieu après que le contenu HTML des images ait été inséré dans le DOM par art-template. Dans le cas contraire, Swiper ne trouverait pas les éléments sur lesquels opérer, et l'effet de carrousel ne serait pas visible.

Étiquettes: Flask Python web development Redis frontend

Publié le 12 juin à 23h14