Skip to content

Commit

Permalink
fix m-m relation with m-o with doted notation
Browse files Browse the repository at this point in the history
  • Loading branch information
dpgaspar committed Jun 6, 2024
1 parent fba2a39 commit 6c45a08
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 25 deletions.
16 changes: 15 additions & 1 deletion examples/crud_rest_api/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import Flask

from flask_appbuilder.extensions import db
from .api import GreetingApi, ContactModelApi, GroupModelApi, ModelOMParentApi
from .api import GreetingApi, ContactModelApi, GroupModelApi, ModelOMParentApi, ContactGroupModelView, ContactGroupTagModelView
from .extensions import appbuilder


Expand All @@ -15,4 +15,18 @@ def create_app() -> Flask:
appbuilder.add_api(ContactModelApi)
appbuilder.add_api(GroupModelApi)
appbuilder.add_api(ModelOMParentApi)
appbuilder.add_view(
ContactGroupModelView,
"List Contact Groups",
icon="fa-folder-open-o",
category="Contacts",
category_icon="fa-envelope",
)
appbuilder.add_view(
ContactGroupTagModelView,
"List Contact Group Tags",
icon="fa-folder-open-o",
category="Contacts",
category_icon="fa-envelope",
)
return app
16 changes: 14 additions & 2 deletions examples/crud_rest_api/app/api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
from flask_appbuilder import ModelRestApi
from flask_appbuilder import ModelRestApi, ModelView
from flask_appbuilder.api import BaseApi, expose
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.models.filters import BaseFilter
from marshmallow import fields, Schema
from sqlalchemy import or_

from .models import Contact, ContactGroup, ModelOMParent
from .models import Contact, ContactGroup, ModelOMParent, ContactGroupTag


class GreetingsResponseSchema(Schema):
message = fields.String()

class ContactGroupModelView(ModelView):
datamodel = SQLAInterface(ContactGroup)
list_columns = ["name", "created_by.first_name", "tags"]
add_columns = ["name", "tags"]
edit_columns = ["name", "tags"]

class ContactGroupTagModelView(ModelView):
datamodel = SQLAInterface(ContactGroupTag)
add_columns = ["name"]
edit_columns = ["name"]
list_columns = ["name"]

class GreetingApi(BaseApi):
resource_name = "greeting"
Expand Down Expand Up @@ -62,6 +73,7 @@ class GroupModelApi(ModelRestApi):
resource_name = "group"
datamodel = SQLAInterface(ContactGroup)
allow_browser_login = True
list_columns = ["name", "tags.name", "created_by.first_name", "created_by.username"]


class ModelOMParentApi(ModelRestApi):
Expand Down
40 changes: 31 additions & 9 deletions examples/crud_rest_api/app/models.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,50 @@
import datetime
import enum
from typing import Optional, List

from flask_appbuilder import Model
from flask_sqlalchemy.model import NameMixin
from sqlalchemy import Column, Date, ForeignKey, Integer, String, Enum
from sqlalchemy.orm import relationship, backref
from flask_appbuilder.models.mixins import AuditMixin
from sqlalchemy import Column, Date, ForeignKey, Integer, String, Enum, Table
from sqlalchemy.orm import relationship, backref, Mapped, mapped_column


mindate = datetime.date(datetime.MINYEAR, 1, 1)


class ContactGroup(NameMixin, Model):
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False)
class ContactGroup(AuditMixin, Model):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)

def __repr__(self):
return self.name


class ContactGroupTag(Model):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(150), unique=True, nullable=False)
group_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("contact_group.id"), nullable=True
)
groups: Mapped[List[ContactGroup]] = relationship(
ContactGroup,
backref=backref("tags"),
secondary="group_tag_association"
)

# Association Table for N-N Relationship
group_tag_association = Table(
'group_tag_association',
Model.metadata,
Column('group_id', Integer, ForeignKey('contact_group.id')),
Column('tag_id', Integer, ForeignKey('contact_group_tag.id'))
)

class Gender(enum.Enum):
Female = 1
Male = 2


class Contact(NameMixin, Model):
class Contact(Model):
id = Column(Integer, primary_key=True)
name = Column(String(150), unique=True, nullable=False)
address = Column(String(564))
Expand All @@ -45,13 +67,13 @@ def year(self):
return datetime.datetime(date.year, 1, 1)


class ModelOMParent(NameMixin, Model):
class ModelOMParent(Model):
__tablename__ = "model_om_parent"
id = Column(Integer, primary_key=True)
field_string = Column(String(50), unique=True, nullable=False)


class ModelOMChild(NameMixin, Model):
class ModelOMChild(Model):
id = Column(Integer, primary_key=True)
field_string = Column(String(50), unique=True, nullable=False)
parent_id = Column(Integer, ForeignKey("model_om_parent.id"))
Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__author__ = "Daniel Vaz Gaspar"
__version__ = "5.0.0rc1"
__version__ = "5.0.0a1"

from .actions import action # noqa: F401
from .api import ModelRestApi # noqa: F401
Expand Down
2 changes: 1 addition & 1 deletion flask_appbuilder/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def post_init(self) -> None:
self.add_permissions()

@property
def app(self):
def app(self) -> Flask:
log.warning(
"appbuilder.app will be deprecated in future versions, "
"use current_app instead"
Expand Down
42 changes: 32 additions & 10 deletions flask_appbuilder/models/sqla/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any, Iterable, Optional, Tuple, Type

from flask import Request
from flask_appbuilder.exceptions import DatabaseException
from flask_appbuilder.exceptions import DatabaseException, FABException
from flask_appbuilder.extensions import db
from flask_appbuilder.filemanager import FileManager, ImageManager
from flask_appbuilder.models.base import BaseInterface
Expand Down Expand Up @@ -297,11 +297,25 @@ def apply_inner_select_joins(
)
return query

def get_outer_query_from_inner_query(
self, query: Query, inner_query: Query
) -> Query:
subquery = inner_query.subquery()
pk = self.get_pk()
pk_name = self.get_pk_name()
if isinstance(pk_name, str):
subquery_pk = getattr(subquery.c, pk_name)
return query.join(subquery, pk == subquery_pk)
if isinstance(pk_name, Iterable):
raise FABException("Composite primary key not supported")
raise FABException("No primary key found")

def apply_outer_select_joins(
self,
query: Query,
select_columns: list[str] | None = None,
outer_default_load: bool = False,
aliases_mapping: dict[str, AliasedClass] | None = None,
) -> Query:
if not select_columns:
return query
Expand Down Expand Up @@ -331,7 +345,11 @@ def apply_outer_select_joins(
.load_only(leaf_column)
)
else:
query = query.options(Load(related_model).load_only(leaf_column))
query = query.options(
Load(self.obj)
.joinedload(getattr(self.obj, root_relation))
.load_only(leaf_column)
)

return query

Expand Down Expand Up @@ -360,12 +378,13 @@ def get_inner_filters(self, filters: Optional[Filters]) -> Filters:

def exists_col_to_many(self, select_columns: list[str]) -> bool:
for column in select_columns:
if is_column_dotted(column):
root_relation = get_column_root_relation(column)
if self.is_relation_many_to_many(
root_relation
) or self.is_relation_one_to_many(root_relation):
return True
if not is_column_dotted(column):
continue
root_relation = get_column_root_relation(column)
if self.is_relation_many_to_many(
root_relation
) or self.is_relation_one_to_many(root_relation):
return True
return False

def get_alias_mapping(
Expand Down Expand Up @@ -456,9 +475,12 @@ def apply_all(
if select_columns and self.exists_col_to_many(select_columns):
if select_columns and order_column:
select_columns = select_columns + [order_column]
outer_query = inner_query._legacy_from_self()
outer_query = self.get_outer_query_from_inner_query(query, inner_query)
outer_query = self.apply_outer_select_joins(
outer_query, select_columns, outer_default_load=outer_default_load
outer_query,
select_columns,
outer_default_load=outer_default_load,
aliases_mapping=aliases_mapping,
)
return self.apply_order_by(outer_query, order_column, order_direction)
else:
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def desc():
package_data={"": ["LICENSE"]},
entry_points={
"flask.commands": ["fab=flask_appbuilder.cli:fab"],
"console_scripts": ["fabmanager = flask_appbuilder.console:cli"],
},
include_package_data=True,
zip_safe=False,
Expand Down

0 comments on commit 6c45a08

Please sign in to comment.