Résolue

[Django] Besoin de conseils pour organiser une base de données

# Fichiers # Bases de données # Django

Salut,

Toujours à propos du projet de gestion de collection de partition, je souhaiterai ajouter un modèle utilisateur. Pour l'instant il n'y a que le modèle 'Score' et les métadonnées 'Genre', 'Instrument', 'Publisher', 'Composer'.

Il faudrait que chaque utilisateur ai accès à son propre catalogue de partition et qu'il puisse intéragire uniquement avec ses partitions et métadonnées.

J'ai essayé de transcrire cela sur un diagramme. Pour rendre privé les différentes tables j'ai ajouté un champ 'owner' dans chacun des modèles concernés et aussi ajouté un modèle 'Catalog' qui contiendra les partitions et sera en liaison OneToOne avec le modèle 'User'.

Pouvez-vous me conseiller, savoir si le diagramme est correcte et s'il doit être amélioré, ou bien si je fais fausse route ?

Autre question, par rapport au stockage des images de partition, il faudrait les organiser en fonction de l'utilisateur, est-il possible de rendre dynamique la valeur donnée au paramètre upload_to, par exemple : upload_to='covers/username' ?

Merci ;-)

Voici le diagramme en question :

Thibault houdon

Mentor

Salut Cam !

Pour être sûr que je comprends bien ce que tu souhaites faire :

Tu confirmes que les partitions ne sont pas forcément reliées à un utilisateur ?

Par exemple tu peux avoir :

Un utilisateur "Patrick".
Un utilisateur "Camille".
La partition (Score) 5e symphonie de Beethoven.

Patrick qui crée un catalogue vide au début, puis qui décide d'y ajouter la partition de la 5e de Beethov.

Camille qui a un catalogue vide.

Est-ce que c'est comme ça que tu vois ton application ? (Des utilisateurs, qui peuvent créer différents catalogues, et ensuite ajouter des partitions existantes sur le site dans leur catalogue, par exemple pour regrouper les partitions de piano qu'ils souhaitent apprendre, les partitions de guitare de musique Rock, etc..) ?

Concernant ta deuxième question oui, tu peux passer une fonction au paramètre upload_to :

from django.contrib.auth.models import User
from django.db import models

def get_upload_path(instance, filename):
    # Tu peux obtenir le nom d'utilisateur à partir de l'objet instance
    username = instance.user.username
    # Et retourner un chemin à partir de son nom d'utilisateur
    return f'covers/{username}/{filename}'

class MyModel(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    # Ici, tu indiques ta fonction get_upload_path
    score_image = models.ImageField(upload_to=get_upload_path)

Salut Thibault,

En fait l'application doit permettre à un utilisateur de pouvoir répertorier sa collection de partition de musique.

Par exemple :

Patrick se crée un compte. Lors de la création, une instance de catalogue se crée et est associée à Patrick, lui seul peut y accéder.
Chaque utilisateur a qu'un seul catalogue et un catalogue ne peut appartenir qu'à un seul utilisateur.

Pour l'instant le catalogue est vide.

À partir de là, Patrick peut ajouter ses partitions et métadonnées. Ces données n'existent pas encore sur le site, l'utilisateur doit les créer.
Concernant les métadonnées, si l'utilisateur le souhaite, l'application propose d'injecter des données dans la base à partir d'une selection qui est stockée dans un fichier JSON. Par exemle, il est possible d'ajouter automatiquement une liste des compositeurs classique les plus populaires, ou une liste d'instrument de musique, etc.

Il n'y a pas de partage de données entre les utilisateurs, chacun possède et gère ses propres partitions et métadonnées. C'est pour cette raison que j'ai pensé mettre un propriétaire sur chaque modèle pour ensuite appliquer un filtre afin de pouvoir afficher les données en fonction de l'utilisateur connecté.

J'espère que mes explications sont assez claires.

Merci pour la réponse à propos du chemin dynamique de l'image.

Bonsoir,

J'ai avancé un peu en suivant le plan du diagramme, pour l'instant ça m'a l'air bon.

Voici où j'en suis :

  • J'ai créer le modèle Catalog avec deux champs : (Je me rend compte qu'une relation plusieurs-à-un serait plus correct pour le champ scores)

    class Catalog(models.Model):
        user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
        scores = models.ManyToManyField(Score, blank=True)
    
        def __str__(self):
            return self.user.username
    

  • Pas besoin de champ catalog_id pour le modèle utilisateur.

  • Lorsqu'un utilisateur s'enregistre, un catalogue se crée automatiquement, l'action se passe dans la vue signup :

    def signup(request):
        if request.method == "POST":
            form = UserRegistrationForm(request.POST)
            if form.is_valid():
                user = form.save()
                Catalog.objects.create(user=user)  # Création du catalogue utilisateur
                login(request, user)
                return redirect("catalog:index")
        else:
            form = UserRegistrationForm()
    
        return render(request, "accounts/signup.html", {"form": form})
    

  • Ajout d'un champ user au modèle partition (Score). Lors de la validation du formulaire, renseignement du champ user puis ajout de la partition dans le catalogue de l'utilisateur :

    class ScoreCreateView(LoginRequiredMixin, CreateView):
        model = Score
        template_name = "catalog/create_or_edit_item.html"
        form_class = ScoreForm
    
        ...
    
        def form_valid(self, form):
            user = self.request.user  # Récupération de l'utilisateur
            score = form.save(commit=False)  # Créer l'instance mais sans sauvegarder
            score.user = user  # Associer l'utilisateur à la partition
            score.save()  # Enregistrement de la partition dans la BDD
    
            user.catalog.scores.add(score)  # Ajout de la partition au catalogue de l'utilisateur
    
            return super().form_valid(form)
    

  • Ensuite dans la vue CatalogIndexView qui hérite de la classe ListView, surcharge de la méthode get_queryset() pour récupérer un QuerySet avec les partitions de l'utilisateur connecté :

    class CatalogIndexView(LoginRequiredMixin, ListView):
        model = Score
        template_name = "catalog/index.html"
        context_object_name = "scores"
    
        def get_queryset(self):
            return super().get_queryset().filter(user=self.request.user)
    

Les partitions affichées sur la page d'index correspondent bien à celle de l'utilisateur :)

Pour l'instant le reste des modèles (Instrument, Genre, Composer, etc) sont partagés entre les utilisateurs mais en réplicant la méthode comme avec Score je pense que ça devrait le faire !

La fonction pour créer le chemin de sauvegarde des covers fonctionne à merveille, encore merci !!

Thibault houdon

Mentor

Salut Cam ! Ça me semble bien parti effectivement ! Tu peux nous montrer ton modèle Score et User ?

Aussi pour la création du catalogue je te conseille de passer par un signal "post_save", je te laisse regarder dans la documentation, c'est très simple à implémenter et ça sera plus robuste que de passer par la vue.

Hello,

J'ai apporté des modifications aux modèles Score et Catalog aujourd'hui, je pense que cette fois ça colle bien avec l'organisation souhaitée.
J'ai douté à garder le modèle Catalog, mais je trouve ça cohérent de le conserver (?)

Les voici :

catalog/models.py :

...
class Catalog(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='catalog')

    def __str__(self):
        return self.user.username


def get_upload_path(instance, filename):
    username = instance.user.username
    return f'covers/{username.lower()}/{filename}'


class Score(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    catalog = models.ForeignKey(Catalog, on_delete=models.CASCADE, related_name='scores')
    title = models.CharField(max_length=120, unique=True)
    subtitle = models.CharField(max_length=120, blank=True)
    slug = models.SlugField(max_length=120, blank=True)
    composer = models.ManyToManyField(Composer, blank=True)
    publisher = models.ForeignKey(Publisher, on_delete=models.SET_NULL, null=True, blank=True)
    instrument = models.ManyToManyField(Instrument, blank=True)
    genre = models.ForeignKey(Genre, on_delete=models.SET_NULL, null=True, blank=True)
    content = models.TextField(max_length=1000, blank=True)
    thumbnail = models.ImageField(upload_to=get_upload_path, default='covers/default.jpg')
    ...

accounts/models.py

class CustomUserManager(BaseUserManager):
    def create_user(self, username, email, password):
        if not email:
            raise ValueError("Vous devez entrer une adresse email.")
        email = self.normalize_email(email)
        user = self.model(username=username)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, username, password=None):
        user = self.create_user(username=username, email=email, password=password)
        user.is_admin = True
        user.is_staff = True
        user.save()
        return user


class CustomUser(AbstractBaseUser):
    username = models.CharField(max_length=150, blank=False, unique=True,)
    email = models.EmailField(max_length=255, unique=True, blank=False)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_admin = models.BooleanField(default=False)

    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email"]
    objects = CustomUserManager()

    def has_perm(self, perm, obj=None):
        return True

    def has_module_perms(self, app_label):
        return True

Pour la création du catalogue, comme tu m'as conseillé, avec l'aide de la doc et d'un "How to" j'ai pu mettre en place le signal post_save.
Ça fonctionne bien, je poste quand même le code pour savoir si tout est ok :

accounts/signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model

from catalog.models import Catalog

user = get_user_model()


@receiver(post_save, sender=user)
def create_user_catalog(sender, instance, created, **kwargs):
    if created:
        Catalog.objects.create(user=instance)

accounts/apps.py

from django.apps import AppConfig


class AccountsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'accounts'

    def ready(self):
        from . import signals

Pour renseigner automatiquement l'utilisateur et le catalogue lors de la création d'une partition, j'ai gardé cette action dans la vue ScoreCreateView. Je ne sais pas s'il est nécessaire ici d'utiliser un signal car on agit direct sur l'instance et non sur une instance d'une autre app ?

class ScoreCreateView(LoginRequiredMixin, CreateView):
    model = Score
    template_name = "catalog/create_or_edit_item.html"
    form_class = ScoreForm

...

    def form_valid(self, form):
        user = self.request.user
        score = form.save(commit=False)
        score.user = user
        score.catalog = user.catalog
        score.save()
        return super().form_valid(form)

Voilà, voilà,
Merci et bonne soirée !

Inscris-toi

(c'est gratuit !)

Inscris-toi

Tu dois créer un compte pour participer aux discussions.

Créer un compte

Rechercher sur le site

Formulaire de contact

Inscris-toi à Docstring

Pour commencer ton apprentissage.

Tu as déjà un compte ? Connecte-toi.