From 7c45b422e6c745bba4ac8e9050d64cbedfddbc3a Mon Sep 17 00:00:00 2001 From: hackish Date: Wed, 13 Sep 2023 13:17:51 -0700 Subject: [PATCH] * feat(mxroute_relay): add mxroute_relay app with initial files and models * feat(mxroute_relay): add server routes and database initialization * feat(mxroute_relay): add admin views for User, Post, Comment, and Dump models * feat(mxroute_relay): add custom ModelView class with pagination and export options * feat(mxroute_relay): add PostView with fields and exclusions * feat(mxroute_relay): add CommentView with exclusions and search/sort options * feat(mxroute_relay): add DumpView with fields and exclusions --- mxroute_relay/app.py | 45 +++++++++++++++++++++++++ mxroute_relay/models.py | 75 +++++++++++++++++++++++++++++++++++++++++ mxroute_relay/views.py | 62 ++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 mxroute_relay/app.py create mode 100644 mxroute_relay/models.py create mode 100644 mxroute_relay/views.py diff --git a/mxroute_relay/app.py b/mxroute_relay/app.py new file mode 100644 index 0000000..17cec81 --- /dev/null +++ b/mxroute_relay/app.py @@ -0,0 +1,45 @@ +from sqlalchemy import create_engine +from sqlmodel import SQLModel +from starlette.applications import Starlette +from starlette.responses import HTMLResponse +from starlette.routing import Route +from starlette_admin.contrib.sqlmodel import Admin +from starlette_admin.contrib.sqlmodel import ModelView + +from .config import ENGINE_URI +from .models import Comment +from .models import Dump +from .models import Post +from .models import User +from .views import CommentView +from .views import DumpView +from .views import PostView + +engine = create_engine(ENGINE_URI, connect_args={"check_same_thread": False}, echo=True) + + +def init_database() -> None: + SQLModel.metadata.create_all(engine) + + +app = Starlette( + routes=[ + Route( + "/", + lambda r: HTMLResponse('Click me to get to Admin!'), + ) + ], + on_startup=[init_database], +) + +# Create admin +admin = Admin(engine, title="Example: SQLModel") + +# Add views +admin.add_view(ModelView(User, icon="fa fa-users")) +admin.add_view(PostView(Post, label="Blog Posts", icon="fa fa-blog")) +admin.add_view(CommentView(Comment, icon="fa fa-comments")) +admin.add_view(DumpView(Dump, icon="fa fa-dumpster")) + +# Mount to admin to app +admin.mount_to(app) diff --git a/mxroute_relay/models.py b/mxroute_relay/models.py new file mode 100644 index 0000000..0467f7d --- /dev/null +++ b/mxroute_relay/models.py @@ -0,0 +1,75 @@ +import enum +from datetime import datetime +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from pydantic import AnyHttpUrl +from pydantic import BaseModel +from pydantic import EmailStr +from pydantic import Field as PydField +from pydantic.color import Color +from sqlalchemy import Column +from sqlalchemy import DateTime +from sqlalchemy import Enum +from sqlalchemy import JSON +from sqlalchemy import String +from sqlalchemy import Text +from sqlmodel import Field +from sqlmodel import Relationship +from sqlmodel import SQLModel + + +class Gender(str, enum.Enum): + MALE = "male" + FEMALE = "female" + UNKNOWN = "unknown" + + +class User(SQLModel, table=True): + id: Optional[int] = Field(primary_key=True) + full_name: str = Field(min_length=3, index=True) + sex: Optional[str] = Field(sa_column=Column(Enum(Gender)), default=Gender.UNKNOWN, index=True) + + posts: List["Post"] = Relationship(back_populates="publisher") + comments: List["Comment"] = Relationship(back_populates="user") + + +class Post(SQLModel, table=True): + id: Optional[int] = Field(primary_key=True) + title: str = Field(min_length=3) + content: str = Field(sa_column=Column(Text)) + tags: List[str] = Field(sa_column=Column(JSON), min_items=1) + published_at: Optional[datetime] = Field(sa_column=Column(DateTime(timezone=True), default=datetime.utcnow)) + + publisher_id: Optional[int] = Field(foreign_key="user.id") + publisher: User = Relationship(back_populates="posts") + + comments: List["Comment"] = Relationship(back_populates="post") + + +class Comment(SQLModel, table=True): + pk: Optional[int] = Field(primary_key=True) + content: str = Field(sa_column=Column(Text), min_length=5) + created_at: Optional[datetime] = Field(sa_column=Column(DateTime(timezone=True), default=datetime.utcnow)) + + post_id: Optional[int] = Field(foreign_key="post.id") + post: Post = Relationship(back_populates="comments") + + user_id: Optional[int] = Field(foreign_key="user.id") + user: User = Relationship(back_populates="comments") + + +class Config(BaseModel): + key: str = PydField(min_length=3) + value: int = PydField(multiple_of=5) + + +class Dump(SQLModel, table=True): + id: Optional[int] = Field(primary_key=True) + email: EmailStr = Field(index=True) + color: Color = Field(sa_column=Column(String(10))) + url: AnyHttpUrl + json_field: Dict[str, Any] = Field(sa_column=Column(JSON)) + configs: List[Config] = Field(sa_column=Column(JSON)) diff --git a/mxroute_relay/views.py b/mxroute_relay/views.py new file mode 100644 index 0000000..4479026 --- /dev/null +++ b/mxroute_relay/views.py @@ -0,0 +1,62 @@ +from starlette_admin import CollectionField +from starlette_admin import ColorField +from starlette_admin import EmailField +from starlette_admin import ExportType +from starlette_admin import IntegerField +from starlette_admin import JSONField +from starlette_admin import ListField +from starlette_admin import StringField +from starlette_admin import URLField +from starlette_admin.contrib.sqlmodel import ModelView + +from .models import Comment +from .models import Post + + +class MyModelView(ModelView): + page_size = 5 + page_size_options = [5, 10, 25 - 1] + export_types = [ExportType.EXCEL, ExportType.CSV] + + +class PostView(MyModelView): + fields = [ + "id", + "title", + "content", + ListField(StringField("tags")), + "published_at", + "publisher", + "comments", + ] + exclude_fields_from_list = [Post.content] + exclude_fields_from_create = [Post.published_at] + exclude_fields_from_edit = ["published_at"] + + +class CommentView(MyModelView): + exclude_fields_from_create = ["created_at"] + exclude_fields_from_edit = ["created_at"] + searchable_fields = [Comment.content, Comment.created_at] + sortable_fields = [Comment.pk, Comment.content, Comment.created_at] + + +class DumpView(MyModelView): + fields = [ + "id", + EmailField("email"), + URLField("url", required=True), + ColorField("color"), + JSONField("json_field"), + ListField( + CollectionField( + "configs", + fields=[ + StringField("key"), + IntegerField("value", help_text="multiple of 5"), + ], + ) + ), + ] + exclude_fields_from_list = ("configs",) + searchable_fields = ("email", "url")