Search code examples
pythonflaskflask-sqlalchemy

Having trouble creating a many-to-many relationship table in my flask blog app


While trying to make my own blog app, I wanted my Users to have roles so different users can access different parts of my blog. To add my Roles model I followed https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html#many-to-many documentation to create an association table along with my Roles Table.

from datetime import datetime, timezone
from typing import Optional
import sqlalchemy as sa 
import sqlalchemy.orm as so
from app import db, login
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from flask_security.models import fsqla_v3 as fsqla
from hashlib import md5

roles_user_table = db.Table(
    "roles_user_table",
    db.metadata, 
    sa.Column("user_id", sa.Integer, sa.ForeignKey("user.id"), primary_key=True),
    sa.Column("roles_id", sa.Integer, sa.ForeignKey("roles.id"), primary_key=True)
)

class User(fsqla.FsUserMixin, db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True)
    password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256))
    posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author')
    about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140))
    last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc))
    role: so.Mapped[list['Roles']] = so.relationship("Roles", secondary=roles_user_table,
                                                        primaryjoin=(roles_user_table.c.user_id == id), 
                                                        secondaryjoin=(roles_user_table.c.roles_id == id),
                                                        back_populates="name")

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def __repr__(self):
        return '<User {}>'.format(self.email)
    
    def avatar(self, size):
        digest = md5(self.email.lower().encode('utf-8')).hexdigest()
        return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'
    
class Post(db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    body: so.Mapped[str] = so.mapped_column(sa.String(140))
    timestamp: so.Mapped[datetime] = so.mapped_column(index=True, default=lambda: datetime.now(timezone.utc))
    user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id), index=True)
    author: so.Mapped[User] = so.relationship(back_populates='posts')

    def __rep__(self):
        return '<Post {}>'.format(self.body)
    
class Roles(db.Model, fsqla.FsRoleMixin):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    name: so.Mapped[User] = so.relationship(secondary=roles_user_table, back_populates="role")

    def __rep__(self):
        return '<Role {}>'.format(self.name)

When I try to add a role in my Roles table by running the command:

admin_role = Roles(name="admin")
sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[User(user)], expression 'Role' failed to locate a name ('Role'). If this is a class name, consider adding this relationship() to the <class 'app.models.User'> class after both dependent classes have been defined.

An I setting up my tables correctly ? or is this issue occurring because of something else ?


Solution

  • You need to use the singular version of Role for your classname.

    Please change this code:

    class Roles(db.Model, fsqla.FsRoleMixin):
    

    to:

    class Role(db.Model, fsqla.FsRoleMixin):