diff --git a/examples/fomanticui/.python-version b/examples/fomanticui/.python-version new file mode 100644 index 0000000000..c8cfe39591 --- /dev/null +++ b/examples/fomanticui/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/examples/fomanticui/README.md b/examples/fomanticui/README.md new file mode 100644 index 0000000000..7d88b0296b --- /dev/null +++ b/examples/fomanticui/README.md @@ -0,0 +1,19 @@ +# FomanticUI Theme +This example contains different types of implementations of each section/feature of flask-admin to test FomanticUI theme. Can also be used to test other upcoming new themes. + +## How to run this example + +Clone the repository and navigate to this example: + +```shell +git clone https://github.com/pallets-eco/flask-admin.git +cd flask-admin/examples/fomanticui +``` + +> This example uses [`uv`](https://docs.astral.sh/uv/) to manage its dependencies and developer environment. + +Run the example using `uv`, which will manage the environment and dependencies automatically: + +```shell +uv run main.py +``` diff --git a/examples/fomanticui/__init__.py b/examples/fomanticui/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/fomanticui/bp.py b/examples/fomanticui/bp.py new file mode 100644 index 0000000000..e723bc3f2a --- /dev/null +++ b/examples/fomanticui/bp.py @@ -0,0 +1,35 @@ +from flask import Blueprint +from flask import redirect +from flask import render_template_string +from flask import url_for + +bp = Blueprint("main", __name__, template_folder="my_admin/templates") + + +@bp.route("/") +def index(): + return render_template_string(""" +

Hello, World!

+ + """) + + +@bp.app_errorhandler(404) +def not_found(error): + return redirect(url_for("main.index")) diff --git a/examples/fomanticui/config.py b/examples/fomanticui/config.py new file mode 100644 index 0000000000..11a55da5d7 --- /dev/null +++ b/examples/fomanticui/config.py @@ -0,0 +1,9 @@ +import os + + +class MyConfig: + DEBUG = True + SECRET_KEY = os.environ.get("SECRET_KEY", os.urandom(16)) + DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///sqlite3-fomanticui.sqlite") + SQLALCHEMY_DATABASE_URI = DATABASE_URL + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/examples/fomanticui/fake.py b/examples/fomanticui/fake.py new file mode 100644 index 0000000000..bdee889594 --- /dev/null +++ b/examples/fomanticui/fake.py @@ -0,0 +1,272 @@ +from random import choice +from random import randint + +from faker import Faker +from models import Address +from models import Category +from models import Comment +from models import db +from models import Department +from models import Employee +from models import Order +from models import OrderItem +from models import OrderStatus +from models import Payment +from models import PaymentMethod +from models import Post +from models import Product +from models import Profile +from models import Project +from models import Role +from models import Tag +from models import User + +fake = Faker() + + +def generate_fake_data(app): + """Populate the database with a rich set of example data covering all models.""" + with app.app_context(): + db.create_all() + db.create_all() + # ----- Roles ----- + if Role.query.count() == 0: + for name in ("admin", "editor", "user"): + db.session.add(Role(name=name)) + db.session.commit() + + roles = Role.query.all() + + # ----- Categories ----- + while Category.query.count() < 5: + db.session.add( + Category(name=fake.unique.word(), description=fake.sentence()) + ) + db.session.commit() + categories = Category.query.all() + + # ----- Tags ----- + while Tag.query.count() < 10: + db.session.add(Tag(name=fake.unique.word())) + db.session.commit() + tags = Tag.query.all() + + # ----- Products ----- + while Product.query.count() < 20: + db.session.add( + Product( + name=fake.unique.word().title(), + description=fake.text(max_nb_chars=200), + price=round( + fake.pyfloat( + left_digits=3, + right_digits=2, + positive=True, + min_value=5, + max_value=200, + ), + 2, + ), + stock=randint(0, 100), + data={ + "color": fake.color_name(), + "size": choice(["S", "M", "L", "XL"]), + }, + ) + ) + db.session.commit() + products = Product.query.all() + + # ----- Users ----- + while User.query.count() < 50: + user = User( + email=fake.unique.email(), + name=fake.name(), + age=randint(18, 65), + active=choice([True, False]), + preferences={"newsletter": choice([True, False])}, + balance=round( + fake.pyfloat(left_digits=3, right_digits=2, positive=True), 2 + ), + last_login=fake.date_time_this_year(), + ) + # assign 1-3 random roles + user.roles.extend( + fake.random_elements(elements=roles, length=randint(1, 3), unique=True) + ) + db.session.add(user) + db.session.commit() + + users = User.query.all() + + # ----- Addresses & Profiles ----- + for user in users: + if not user.addresses: + addr = Address( + street=fake.street_address(), + city=fake.city(), + state=fake.state(), + postal_code=fake.postcode(), + country=fake.country(), + is_primary=True, + user_id=user.id, + ) + db.session.add(addr) + if not user.profile: + profile = Profile(bio=fake.sentence(), avatar=None, user_id=user.id) + db.session.add(profile) + db.session.commit() + + # ----- Posts ----- + while Post.query.count() < 100: + author = choice(users) + post = Post( + author_id=author.id, + category=choice(categories), + title=fake.sentence(), + body=fake.text(max_nb_chars=800), + ) + post.tags = fake.random_elements( + elements=tags, length=randint(1, 4), unique=True + ) + db.session.add(post) + db.session.commit() + + posts = Post.query.all() + + # ----- Comments ----- + while Comment.query.count() < 300: + db.session.add( + Comment( + post_id=choice(posts).id, + user_id=choice(users).id, + body=fake.sentence(), + ) + ) + db.session.commit() + + # ----- Orders, Items, Payments ----- + while Order.query.count() < 200: + user = choice(users) + order = Order( + user_id=user.id, + status=choice(list(OrderStatus)), + total_price=0, # will update after adding items + ) + db.session.add(order) + db.session.commit() + + orders = Order.query.all() + + for order in orders: + if not order.items: + chosen_products = fake.random_elements( # type: ignore + elements=products, length=randint(1, 5), unique=True + ) + total = 0 + for prod in chosen_products: + qty = randint(1, 3) + total += prod.price * qty + db.session.add( + OrderItem( + order_id=order.id, + product_id=prod.id, + quantity=qty, + price=prod.price, + ) + ) + order.total_price = round(total, 2) + db.session.add( + Payment( + order_id=order.id, + amount=order.total_price, + paid_at=fake.date_time_this_year(), + method=choice(list(PaymentMethod)), + ) + ) + db.session.commit() + + # ----- Departments ----- + while Department.query.count() < 5: + db.session.add( + Department( + name=fake.unique.company(), + budget=round( + fake.pyfloat( + left_digits=7, + right_digits=2, + positive=True, + min_value=10000, + max_value=1000000, + ), + 2, + ), + ) + ) + db.session.commit() + departments = Department.query.all() + + # ----- Projects ----- + while Project.query.count() < 10: + db.session.add( + Project( + name=fake.unique.catch_phrase(), + deadline=fake.date_this_year(after_today=True), + description=fake.text(max_nb_chars=200), + budget=round( + fake.pyfloat( + left_digits=6, + right_digits=2, + positive=True, + min_value=5000, + max_value=500000, + ), + 2, + ), + ) + ) + db.session.commit() + projects = Project.query.all() + + # ----- Employees ----- + while Employee.query.count() < 100: + dept = choice(departments) + emp = Employee( + department_id=dept.id, + first_name=fake.first_name(), + last_name=fake.last_name(), + salary=round( + fake.pyfloat( + left_digits=6, + right_digits=2, + positive=True, + min_value=30000, + max_value=150000, + ), + 2, + ), + hire_date=fake.date_between(start_date="-10y", end_date="today"), + shift_start=fake.time_object(), + is_full_time=choice([True, False]), + rating=round( + fake.pyfloat( + left_digits=1, + right_digits=2, + positive=True, + min_value=1, + max_value=5, + ), + 2, + ), + ) + db.session.add(emp) + db.session.commit() + + employees = Employee.query.all() + + for emp in employees: + if not emp.projects: + emp.projects = fake.random_elements( + elements=projects, length=randint(1, 4), unique=True + ) + db.session.commit() diff --git a/examples/fomanticui/main.py b/examples/fomanticui/main.py new file mode 100644 index 0000000000..46879a724b --- /dev/null +++ b/examples/fomanticui/main.py @@ -0,0 +1,27 @@ +from bp import bp +from config import MyConfig +from fake import generate_fake_data +from flask import Flask +from models import db_init +from my_admin.basic_admin import admin as basic_admin +from my_admin.pro_admin import admin as pro_admin +from my_admin.super_admin import admin as super_admin + +app = Flask(__name__) + +app.config.from_object(MyConfig) +db_init(app) +generate_fake_data(app) + +# Init admin instances + +basic_admin.init_app(app) +super_admin.init_app(app) +pro_admin.init_app(app) + + +app.register_blueprint(bp) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/examples/fomanticui/models.py b/examples/fomanticui/models.py new file mode 100644 index 0000000000..c8b873bfee --- /dev/null +++ b/examples/fomanticui/models.py @@ -0,0 +1,316 @@ +import enum + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import Boolean +from sqlalchemy import Column +from sqlalchemy import Date +from sqlalchemy import DateTime +from sqlalchemy import Enum +from sqlalchemy import Float +from sqlalchemy import ForeignKey +from sqlalchemy import func +from sqlalchemy import Integer +from sqlalchemy import JSON +from sqlalchemy import LargeBinary +from sqlalchemy import Numeric +from sqlalchemy import String +from sqlalchemy import Table +from sqlalchemy import Text +from sqlalchemy import Time +from sqlalchemy.orm import relationship + +db = SQLAlchemy() + +# ----- Association tables ----- +user_roles = Table( + "user_roles", + db.Model.metadata, + Column("user_id", Integer, ForeignKey("user.id"), primary_key=True), + Column("role_id", Integer, ForeignKey("role.id"), primary_key=True), +) + +posts_tags = Table( + "posts_tags", + db.Model.metadata, + Column("post_id", Integer, ForeignKey("post.id"), primary_key=True), + Column("tag_id", Integer, ForeignKey("tag.id"), primary_key=True), +) + +# New association table for many-to-many relationship between employees and projects +employees_projects = Table( + "employees_projects", + db.Model.metadata, + Column("employee_id", Integer, ForeignKey("employee.id"), primary_key=True), + Column("project_id", Integer, ForeignKey("project.id"), primary_key=True), +) + +# ----- Enumerations ----- + + +class OrderStatus(enum.Enum): + pending = "pending" + paid = "paid" + shipped = "shipped" + completed = "completed" + cancelled = "cancelled" + + +class PaymentMethod(enum.Enum): + credit_card = "credit_card" + paypal = "paypal" + stripe = "stripe" + + +class User(db.Model): + id = Column(Integer, primary_key=True) + email = Column(String(120), unique=True) + name = Column(String(64), nullable=False) + age = Column(Integer, nullable=False) + active = Column(Boolean, default=False) + + created_at = Column(DateTime, default=func.now()) + last_login = Column(DateTime) + + preferences = Column(JSON) + balance = Column(Numeric(10, 2), default=0) + + # Relationships + posts = relationship("Post", back_populates="author") + addresses = relationship( + "Address", back_populates="user", cascade="all, delete-orphan" + ) + profile = relationship( + "Profile", back_populates="user", uselist=False, cascade="all, delete-orphan" + ) + roles = relationship("Role", secondary=user_roles, back_populates="users") + orders = relationship("Order", back_populates="user") + + def __repr__(self): + return f"" + + +class Post(db.Model): + id = Column(Integer, primary_key=True) + author_id = Column(Integer, ForeignKey("user.id")) + category_id = Column(Integer, ForeignKey("category.id")) + + title = Column(String(128), nullable=False) + body = Column(Text) + + created_at = Column(DateTime, default=func.now()) + + author = relationship("User", back_populates="posts") + category = relationship("Category", back_populates="posts") + tags = relationship("Tag", secondary=posts_tags, back_populates="posts") + comments = relationship( + "Comment", back_populates="post", cascade="all, delete-orphan" + ) + attachments = relationship( + "Attachment", back_populates="post", cascade="all, delete-orphan" + ) + + def __repr__(self): + return f"" + + +# ----- Extra Models ----- + + +class Role(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(32), unique=True, nullable=False) + + users = relationship("User", secondary=user_roles, back_populates="roles") + + def __repr__(self): + return f"" + + +class Address(db.Model): + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("user.id")) + street = Column(String(128)) + city = Column(String(64)) + state = Column(String(32)) + postal_code = Column(String(20)) + country = Column(String(64)) + is_primary = Column(Boolean, default=False) + + user = relationship("User", back_populates="addresses") + + def __repr__(self): + return f"
" + + +class Profile(db.Model): + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("user.id"), unique=True) + bio = Column(Text) + avatar = Column(LargeBinary) + + user = relationship("User", back_populates="profile") + + def __repr__(self): + return f"" + + +class Category(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(64), unique=True, nullable=False) + description = Column(Text) + + posts = relationship("Post", back_populates="category") + + def __repr__(self): + return f"" + + +class Tag(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(64), unique=True, nullable=False) + + posts = relationship("Post", secondary=posts_tags, back_populates="tags") + + def __repr__(self): + return f"" + + +class Comment(db.Model): + id = Column(Integer, primary_key=True) + post_id = Column(Integer, ForeignKey("post.id")) + user_id = Column(Integer, ForeignKey("user.id")) + body = Column(Text) + created_at = Column(DateTime, default=func.now()) + + post = relationship("Post", back_populates="comments") + user = relationship("User") + + def __repr__(self): + return f"" + + +class Attachment(db.Model): + id = Column(Integer, primary_key=True) + post_id = Column(Integer, ForeignKey("post.id")) + filename = Column(String(128)) + data = Column(LargeBinary) + mimetype = Column(String(32)) + + post = relationship("Post", back_populates="attachments") + + def __repr__(self): + return f"" + + +class Product(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(64), nullable=False) + description = Column(Text) + price = Column(Numeric(10, 2), nullable=False) + stock = Column(Integer, default=0) + data = Column(JSON) + + def __repr__(self): + return f"" + + +class Order(db.Model): + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("user.id")) + status = Column(Enum(OrderStatus), default=OrderStatus.pending) # type: ignore + total_price = Column(Numeric(10, 2), default=0) + created_at = Column(DateTime, default=func.now()) + + user = relationship("User", back_populates="orders") + items = relationship( + "OrderItem", back_populates="order", cascade="all, delete-orphan" + ) + payments = relationship( + "Payment", back_populates="order", cascade="all, delete-orphan" + ) + + def __repr__(self): + return f"" + + +class OrderItem(db.Model): + id = Column(Integer, primary_key=True) + order_id = Column(Integer, ForeignKey("order.id")) + product_id = Column(Integer, ForeignKey("product.id")) + quantity = Column(Integer, default=1) + price = Column(Numeric(10, 2), nullable=False) + + order = relationship("Order", back_populates="items") + product = relationship("Product") + + def __repr__(self): + return f"" + + +class Payment(db.Model): + id = Column(Integer, primary_key=True) + order_id = Column(Integer, ForeignKey("order.id")) + amount = Column(Numeric(10, 2), nullable=False) + paid_at = Column(DateTime, default=func.now()) + method = Column(Enum(PaymentMethod)) # type: ignore + + order = relationship("Order", back_populates="payments") + + def __repr__(self): + return f"" + + +# ---------------- New models for more extensive +# type & relationship coverage ---------------- +class Department(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(64), unique=True, nullable=False) + budget = Column(Numeric(12, 2), default=0) + created_at = Column(DateTime, default=func.now()) + + employees = relationship("Employee", back_populates="department") + + def __repr__(self): + return f"" + + +class Project(db.Model): + id = Column(Integer, primary_key=True) + name = Column(String(128), unique=True, nullable=False) + deadline = Column(Date) + description = Column(Text) + budget = Column(Numeric(12, 2)) + + employees = relationship( + "Employee", secondary=employees_projects, back_populates="projects" + ) + + def __repr__(self): + return f"" + + +class Employee(db.Model): + id = Column(Integer, primary_key=True) + department_id = Column(Integer, ForeignKey("department.id")) + first_name = Column(String(64), nullable=False) + last_name = Column(String(64), nullable=False) + salary = Column(Numeric(10, 2)) + hire_date = Column(Date) + shift_start = Column(Time) + is_full_time = Column(Boolean, default=True) + rating = Column(Float) + + department = relationship("Department", back_populates="employees") + projects = relationship( + "Project", secondary=employees_projects, back_populates="employees" + ) + + def __repr__(self): + return f"" + + +def db_init(app: Flask): + with app.app_context(): + db.init_app(app) + db.create_all() diff --git a/examples/fomanticui/my_admin/__init__.py b/examples/fomanticui/my_admin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/fomanticui/my_admin/basic_admin.py b/examples/fomanticui/my_admin/basic_admin.py new file mode 100644 index 0000000000..739046624e --- /dev/null +++ b/examples/fomanticui/my_admin/basic_admin.py @@ -0,0 +1,49 @@ +from flask import flash +from flask_admin import actions +from flask_admin import Admin +from flask_admin.contrib.sqla import ModelView +from flask_admin.theme import FomanticUI +from models import db +from models import Post +from models import User + +from .views import IndexView + + +class CustomUserModelView(ModelView): + column_editable_list = ["active"] + can_view_details = True + can_create = False + can_delete = False + can_edit = False + page_size_options = (1, 5, 10, 20) + simple_list_pager = True + can_set_page_size = True + page_size = 5 + column_default_sort = "age" + column_list = ("age", "email", "name", "age", "active", "balance") + + @actions.action( + "custom_action", "Custom Action", "Are you sure you want to do this?" + ) + def custom_action(self, ids): + flash("Done!") + + +class PostModelView(ModelView): + can_create = False + can_delete = False + can_edit = False + column_list = ["id", "title", "body", "author", "created_at"] + column_editable_list = ["title"] + + +admin = Admin( + name="BasicAdmin", + theme=FomanticUI(base_template="master.html"), + index_view=IndexView("Home", endpoint="basicadmin", url="/basicadmin"), +) + + +admin.add_view(CustomUserModelView(User, db.session, name="Users", endpoint="b/users")) +admin.add_view(PostModelView(Post, db.session, name="Posts", endpoint="b/posts")) diff --git a/examples/fomanticui/my_admin/pro_admin.py b/examples/fomanticui/my_admin/pro_admin.py new file mode 100644 index 0000000000..596f50e487 --- /dev/null +++ b/examples/fomanticui/my_admin/pro_admin.py @@ -0,0 +1,358 @@ +import os + +from flask import flash +from flask_admin import actions +from flask_admin import Admin +from flask_admin.contrib import rediscli +from flask_admin.contrib.fileadmin import FileAdmin +from flask_admin.contrib.sqla import ModelView +from flask_admin.menu import MenuLink +from flask_admin.model.form import InlineFormAdmin +from flask_admin.theme import FomanticUI +from models import Address +from models import Attachment +from models import Category +from models import Comment +from models import db +from models import Department +from models import Employee +from models import Order +from models import OrderItem +from models import Payment +from models import Post +from models import Product +from models import Project +from models import Role +from models import Tag +from models import User +from redis import Redis + +from .views import IndexView + +# ---------------- Generic helpers ---------------- + + +class ReadOnlyModelView(ModelView): + """A view that disallows create / edit / delete to test read-only mode.""" + + can_create = False + can_edit = False + can_delete = False + can_view_details = True + can_export = True + export_types = ["csv", "json", "xls", "xlsx", "html", "pdf"] + page_size_options = (5, 10, 25, 50, 100) + + +# ---------------- Inline helpers ---------------- + + +class CommentInline(InlineFormAdmin): + form_columns = ("id", "user", "body", "created_at") + + +class AddressInline(InlineFormAdmin): + form_columns = ( + "id", + "street", + "city", + "state", + "postal_code", + "country", + "is_primary", + ) + + +class OrderItemInline(InlineFormAdmin): + form_columns = ("id", "product", "quantity", "price") + + +class PaymentInline(InlineFormAdmin): + form_columns = ("id", "amount", "method", "paid_at") + + +# ---------------- Concrete model views ---------------- + + +class ExtendedUserView(ModelView): + column_list = ( + "id", + "email", + "name", + "age", + "active", + "created_at", + "balance", + ) + inline_models = (AddressInline(Address),) + column_searchable_list = ("email", "name") + column_filters = ("active", "age", "created_at", "balance") + column_editable_list = ("active",) + column_default_sort = ("created_at", True) + can_export = True + export_types = ["csv", "json", "xlsx"] + page_size_options = (5, 10, 25, 50) + + @actions.action("activate", "Activate", "Activate selected users?") + def action_activate(self, ids): + qry = User.query.filter(User.id.in_(ids)) + count = 0 + for u in qry.all(): + if not u.active: + u.active = True + count += 1 + db.session.commit() + flash(f"Activated {count} users!", "success") + + +class PostFullView(ModelView): + column_list = ( + "id", + "title", + "author", + "category", + "created_at", + "tags", + ) + column_searchable_list = ("title", "body") + column_editable_list = ("title", "category", "author", "tags", "created_at") + column_filters = ("category", "created_at", "tags") + column_formatters = { + "title": lambda v, c, m, p: m.title[:40] + "…" + if len(m.title) > 40 + else m.title, + } + inline_models = [CommentInline(Comment), Attachment] + form_excluded_columns = ("created_at",) + page_size_options = (10, 25, 50) + + +class RoleView(ReadOnlyModelView): + column_searchable_list = ("name",) + + +class CategoryView(ModelView): + column_list = ("id", "name", "description") + column_searchable_list = ("name",) + # form_overrides = {"description": "ckeditor"} + + +class TagView(ReadOnlyModelView): + column_searchable_list = ("name",) + + +class CommentView(ReadOnlyModelView): + column_filters = ("created_at", "post") + + +class AttachmentView(ReadOnlyModelView): + column_list = ("id", "filename", "mimetype", "post") + can_create = True + + +class ProductView(ModelView): + column_list = ("id", "name", "price", "stock", "data") + column_filters = ("price", "stock") + column_editable_list = ("price", "stock") + can_export = True + export_types = ["xlsx", "csv", "json"] + can_edit = True + edit_modal = True + can_create = True + + +class OrderView(ModelView): + column_list = ("id", "user", "status", "total_price", "created_at") + column_filters = ("status", "created_at", "total_price") + inline_models = [OrderItemInline(OrderItem), PaymentInline(Payment)] + can_set_page_size = True + page_size_options = (10, 25, 100) + column_default_sort = ("created_at", True) + + +class OrderItemView(ReadOnlyModelView): + column_list = ("id", "order", "product", "quantity", "price") + + +class PaymentView(ReadOnlyModelView): + column_list = ("id", "order", "amount", "method", "paid_at") + column_filters = ("method",) + + +class DepartmentView(ModelView): + column_list = ("id", "name", "budget", "created_at") + column_searchable_list = ("name",) + column_filters = ("budget",) + column_editable_list = ("budget",) + + +class EmployeeView(ModelView): + column_list = ( + "id", + "first_name", + "last_name", + "department", + "salary", + "hire_date", + "rating", + ) + column_searchable_list = ("first_name", "last_name") + column_filters = ("department", "salary", "hire_date", "rating") + form_ajax_refs = {"department": {"fields": ("name",)}} + column_editable_list = ("salary", "rating") + page_size_options = (10, 25, 50, 100) + + +class ProjectView(ModelView): + column_list = ("id", "name", "deadline", "budget") + column_filters = ("deadline", "budget") + column_searchable_list = ("name",) + form_columns = ("employees", "name", "deadline", "budget") + # form_ajax_refs = {"employees": {"fields": ("first_name", "last_name")}} + + +# ----------------- Admin instance ----------------- + +admin = Admin( + name="ProAdmin", + theme=FomanticUI(base_template="master.html"), + index_view=IndexView("Home", endpoint="proadmin", url="/proadmin"), +) + + +admin.add_link( + MenuLink( + name="Support", + url="https://discord.gg/flask", + icon_type="default", + icon_value="life ring", + ) +) + +# Files +_path_filemanager = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) +admin.add_view( + FileAdmin(_path_filemanager, name="File Manager", endpoint="p/filemanager") +) + +# Redis console +v = rediscli.RedisCli(Redis(), "Redis Console") +v.menu_icon_value = "terminal" +admin.add_view(v) + +# General data +admin.add_view(ExtendedUserView(User, db.session, name="Users", endpoint="p/users")) +admin.add_view( + RoleView(Role, db.session, name="Roles", endpoint="p/roles", category="Reference") +) +admin.add_view( + CategoryView( + Category, + db.session, + name="Categories", + endpoint="p/categories", + category="Reference", + ) +) +admin.add_view( + TagView(Tag, db.session, name="Tags", endpoint="p/tags", category="Reference") +) + +# Blog +admin.add_view( + PostFullView(Post, db.session, name="Posts", endpoint="p/posts", category="Blog") +) +admin.add_view( + CommentView( + Comment, db.session, name="Comments", endpoint="p/comments", category="Blog" + ) +) +admin.add_view( + AttachmentView( + Attachment, + db.session, + name="Attachments", + endpoint="p/attachments", + category="Blog", + ) +) + +# Shop +admin.add_view( + ProductView( + Product, + db.session, + name="Products", + endpoint="p/products", + category="E-Commerce", + ) +) +admin.add_view( + OrderView( + Order, db.session, name="Orders", endpoint="p/orders", category="E-Commerce" + ) +) +admin.add_view( + OrderItemView( + OrderItem, + db.session, + name="Order Items", + endpoint="p/orderitems", + category="E-Commerce", + ) +) +admin.add_view( + PaymentView( + Payment, + db.session, + name="Payments", + endpoint="p/payments", + category="E-Commerce", + ) +) + +admin.add_link( + MenuLink( + name="Documentation", + url="https://flask-admin.readthedocs.io/", + icon_type="default", + icon_value="book", + ) +) +admin.add_link( + MenuLink( + name="GitHub", + url="https://github.com/flask-admin/flask-admin", + icon_type="default", + icon_value="github", + ) +) +admin.add_link( + MenuLink( + name="Support", + url="https://discord.gg/flask", + icon_type="default", + icon_value="life ring", + ) +) + +# HR +admin.add_view( + DepartmentView( + Department, + db.session, + name="Departments", + endpoint="p/departments", + category="HR", + ) +) +admin.add_view( + EmployeeView( + Employee, db.session, name="Employees", endpoint="p/employees", category="HR" + ) +) +admin.add_view( + ProjectView( + Project, db.session, name="Projects", endpoint="p/projects", category="HR" + ) +) diff --git a/examples/fomanticui/my_admin/super_admin.py b/examples/fomanticui/my_admin/super_admin.py new file mode 100644 index 0000000000..f0c92b031b --- /dev/null +++ b/examples/fomanticui/my_admin/super_admin.py @@ -0,0 +1,223 @@ +import os + +from flask_admin import Admin +from flask_admin.contrib import rediscli +from flask_admin.contrib.fileadmin import FileAdmin +from flask_admin.contrib.sqla import ModelView +from flask_admin.theme import FomanticUI +from models import db +from models import Post +from models import User +from redis import Redis +from wtforms.fields import StringField + +from .views import IndexView + + +class UserModelView(ModelView): + column_list = ["id", "email", "name", "age", "active", "created_at"] + column_editable_list = ["active"] + can_view_details = True + can_set_page_size = True + can_export = True + export_types = ["pdf", "html", "json", "csv", "xlsx"][:1] + column_filters = ("id", "email", "name", "age", "active", "created_at", "posts") + page_size_options = (1, 5, 10, 20, 2000) + column_searchable_list = ("email", "name", "id") + column_descriptions = dict( + email="User email address - this must be a valid email format and " + "will be used for account verification and password reset requests", + name="Full name of the user as it should appear on their profile " + "and in communications. This field should contain the " + "user's complete legal name or preferred display name, which " + "will be used throughout the application interface, email notifications," + "and public-facing components. The name should be entered in a consistent " + "format to maintain data quality and enable proper sorting and filtering " + "capabilities. Special characters and numbers are allowed but should be used " + "appropriately to reflect the user's actual name rather than nicknames " + "or aliases", + age="User age in years (must be 13 or older to comply with data" + "protection regulations)", + active="Whether the user account is active - inactive accounts " + "cannot log in or perform any actions until reactivated by an administrator", + created_at="Timestamp indicating when this user account " + "was first created in the system", + posts="Collection of all blog posts authored by this user - can be " + "filtered and sorted by date, title or status", + ) + form_extra_fields = { + "Extra field": StringField( + "Extra Field Name", render_kw={"dddd": 1, "append": "sdsd"} + ) + } + + +class UserModelViewModal(UserModelView): + create_modal = True + edit_modal = True + details_modal = True + delete_modal = True + + +class PostModelView(ModelView): + column_list = ["id", "title", "body", "author", "created_at"] + column_editable_list = ["title"] + can_view_details = True + can_set_page_size = True + can_export = True + column_sortable_list = ["id"] + export_types = ["pdf", "html", "json", "csv", "xlsx"] + column_filters = ("id", "title", "body", "author", "created_at") + column_searchable_list = ("title", "body") + column_formatters = { + "body": lambda v, c, m, p: m.body[:50] + "..." if m.body else "" + } + column_descriptions = dict( + title="Post Title Description", + body="Post Body Description", + author="Author Description", + created_at="Created At Description", + ) + + +class PostModelViewModal(PostModelView): + create_modal = True + edit_modal = True + details_modal = True + delete_modal = True + + +class MyFileAdmin(FileAdmin): + rename_modal = True + mkdir_modal = True + upload_modal = True + edit_modal = True + can_rename = True + can_delete_dirs = True + can_mkdir = True + can_download = True + can_delete = True + can_upload = True + + +class PlainFileAdmin(FileAdmin): + can_rename = True + can_delete_dirs = True + can_mkdir = True + can_download = True + can_delete = True + can_upload = True + + +admin = Admin( + name="SuperAdmin", + theme=FomanticUI(base_template="master.html"), + index_view=IndexView("Home", endpoint="superadmin", url="/superadmin"), +) + +v = rediscli.RedisCli(Redis(), "Redis Console", endpoint="s/rediscli") +v.menu_icon_value = "terminal" +admin.add_view(v) + +_path_filemanager = os.path.join(os.path.dirname(__file__), "../") + +admin.add_view( + PlainFileAdmin( + _path_filemanager, + name="File Manager (Standart)", + endpoint="s/filemanager", + menu_icon_value="folder open", + category="File Management", + ) +) +admin.add_view( + MyFileAdmin( + _path_filemanager, + name="File Manager (Modal)", + endpoint="s/filemanager_modal", + menu_icon_value="file alternate", + category="File Management", + ) +) + +# Add a category for user views +admin.add_category("User Management", icon_value="users") + +# Add both user model views (normal and modal) to the "User Management" category +admin.add_view( + UserModelView( + User, + db.session, + name="Users (Standard)", + endpoint="s/user", + menu_icon_type="default", + menu_icon_value="user", + category="User Management", + ) +) +admin.add_view( + UserModelViewModal( + User, + db.session, + name="Users (Modal)", + endpoint="s/user_modal", + menu_icon_type="default", + menu_icon_value="user outline", + category="User Management", + ) +) + +# Add both post model views (normal and modal) to a "Post Management" category +admin.add_category("Post Management", icon_value="newspaper") +admin.add_view( + PostModelView( + Post, + db.session, + name="Posts (Standard)", + endpoint="s/post", + menu_icon_type="default", + menu_icon_value="newspaper", + category="Post Management", + ) +) +admin.add_view( + PostModelViewModal( + Post, + db.session, + name="Posts (Modal)", + endpoint="s/post_modal", + menu_icon_type="default", + menu_icon_value="newspaper outline", + category="Post Management", + ) +) + +admin.add_category( + "Other Models", + icon_value="cubes", # the icon name, e.g., FontAwesome 'cubes' +) + +admin.add_view( + PostModelView( + Post, + db.session, + name="Other Posts", + endpoint="s/otherposts", + category="Other Models", + ) +) +admin.add_view( + UserModelView( + User, + db.session, + name="Other Users", + endpoint="s/otherusers", + category="Other Models", + ) +) + + +for i in range(1, 21): + admin.add_view( + PostModelView(Post, db.session, name=f"Posts {i}", endpoint=f"s/posts_{i}") + ) diff --git a/examples/fomanticui/my_admin/templates/index.html b/examples/fomanticui/my_admin/templates/index.html new file mode 100644 index 0000000000..60b1370557 --- /dev/null +++ b/examples/fomanticui/my_admin/templates/index.html @@ -0,0 +1,18 @@ +{% extends admin_base_template %} +{% block body %} +
+

+ {{ admin_view.admin.name }} with "{{ admin_view.admin.theme.__str__().title() }}" +

+
Go through the views.
+
+ Go to BasicAdmin +
+
+ Go to SuperAdmin +
+
+ Go to ProAdmin +
+
+{% endblock %} diff --git a/examples/fomanticui/my_admin/templates/master.html b/examples/fomanticui/my_admin/templates/master.html new file mode 100644 index 0000000000..2e55c4f385 --- /dev/null +++ b/examples/fomanticui/my_admin/templates/master.html @@ -0,0 +1,36 @@ +{% extends 'admin/base.html' %} + +{% block brand %} + + {{ admin_view.admin.name }} +{% endblock %} + +{% block access_control %} + +{% endblock %} diff --git a/examples/fomanticui/my_admin/views.py b/examples/fomanticui/my_admin/views.py new file mode 100644 index 0000000000..0dd6d02615 --- /dev/null +++ b/examples/fomanticui/my_admin/views.py @@ -0,0 +1,8 @@ +from flask_admin import AdminIndexView +from flask_admin import expose + + +class IndexView(AdminIndexView): + @expose("/") + def index(self): + return self.render("index.html") diff --git a/examples/fomanticui/pyproject.toml b/examples/fomanticui/pyproject.toml new file mode 100644 index 0000000000..01bddb6026 --- /dev/null +++ b/examples/fomanticui/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "example-fomanticui" +version = "0.1.0" +description = "Forms Files and Images Example." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "flask-admin[sqlalchemy-with-utils, export]", + "blinker==1.9.0", + "click==8.3.0", + "colorama==0.4.6", + "Faker==37.12.0", + "Flask==3.1.2", + "Flask-SQLAlchemy==3.1.1", + "greenlet==3.2.4", + "itsdangerous==2.2.0", + "Jinja2==3.1.6", + "MarkupSafe==3.0.3", + "redis==7.0.1", + "SQLAlchemy==2.0.44", + "typing_extensions==4.15.0", + "tzdata==2025.2", + "Werkzeug==3.1.3", + "WTForms==3.2.1", +] + +[tool.uv.sources] +flask-admin = { path = "../../", editable = true } diff --git a/examples/fomanticui/uv.lock b/examples/fomanticui/uv.lock new file mode 100644 index 0000000000..b8de70915a --- /dev/null +++ b/examples/fomanticui/uv.lock @@ -0,0 +1,633 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colour" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/d4/5911a7618acddc3f594ddf144ecd8a03c29074a540f4494670ad8f153efe/colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee", size = 24776, upload-time = "2017-11-19T23:20:08.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/46/e81907704ab203206769dee1385dc77e1407576ff8f50a0681d0a6b541be/colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c", size = 23772, upload-time = "2017-11-19T23:20:04.56Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "example-fomanticui" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "colorama" }, + { name = "faker" }, + { name = "flask" }, + { name = "flask-admin", extra = ["export", "sqlalchemy-with-utils"] }, + { name = "flask-sqlalchemy" }, + { name = "greenlet" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "redis" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, + { name = "tzdata" }, + { name = "werkzeug" }, + { name = "wtforms" }, +] + +[package.metadata] +requires-dist = [ + { name = "blinker", specifier = "==1.9.0" }, + { name = "click", specifier = "==8.3.0" }, + { name = "colorama", specifier = "==0.4.6" }, + { name = "faker", specifier = "==37.12.0" }, + { name = "flask", specifier = "==3.1.2" }, + { name = "flask-admin", extras = ["sqlalchemy-with-utils", "export"], editable = "../../" }, + { name = "flask-sqlalchemy", specifier = "==3.1.1" }, + { name = "greenlet", specifier = "==3.2.4" }, + { name = "itsdangerous", specifier = "==2.2.0" }, + { name = "jinja2", specifier = "==3.1.6" }, + { name = "markupsafe", specifier = "==3.0.3" }, + { name = "redis", specifier = "==7.0.1" }, + { name = "sqlalchemy", specifier = "==2.0.44" }, + { name = "typing-extensions", specifier = "==4.15.0" }, + { name = "tzdata", specifier = "==2025.2" }, + { name = "werkzeug", specifier = "==3.1.3" }, + { name = "wtforms", specifier = "==3.2.1" }, +] + +[[package]] +name = "faker" +version = "37.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/84/e95acaa848b855e15c83331d0401ee5f84b2f60889255c2e055cb4fb6bdf/faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa", size = 1935741, upload-time = "2025-10-24T15:19:58.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "flask-admin" +version = "2.0.1" +source = { editable = "../../" } +dependencies = [ + { name = "flask" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "werkzeug" }, + { name = "wtforms" }, +] + +[package.optional-dependencies] +export = [ + { name = "tablib" }, +] +sqlalchemy-with-utils = [ + { name = "arrow" }, + { name = "colour" }, + { name = "email-validator" }, + { name = "flask-sqlalchemy" }, + { name = "sqlalchemy" }, + { name = "sqlalchemy-citext" }, + { name = "sqlalchemy-utils" }, +] + +[package.metadata] +requires-dist = [ + { name = "arrow", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=0.14.0" }, + { name = "azure-storage-blob", marker = "extra == 'azure-blob-storage'", specifier = ">=12.0.0" }, + { name = "boto3", marker = "extra == 's3'", specifier = ">=1.33" }, + { name = "colour", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=0.1.5" }, + { name = "email-validator", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=2" }, + { name = "flask", specifier = ">=2.0" }, + { name = "flask-admin", extras = ["azure-blob-storage"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["export"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["geoalchemy"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["images"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["mongoengine"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["peewee"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["pymongo"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["rediscli"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["s3"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["sqlalchemy"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["sqlalchemy"], marker = "extra == 'geoalchemy'", editable = "../../" }, + { name = "flask-admin", extras = ["sqlalchemy"], marker = "extra == 'sqlalchemy-with-utils'", editable = "../../" }, + { name = "flask-admin", extras = ["sqlalchemy-with-utils"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-admin", extras = ["translation"], marker = "extra == 'all'", editable = "../../" }, + { name = "flask-babel", marker = "extra == 'translation'", specifier = ">=3.0.1" }, + { name = "flask-sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=3" }, + { name = "geoalchemy2", marker = "extra == 'geoalchemy'", specifier = ">=0.14.0" }, + { name = "jinja2", specifier = ">=3.0" }, + { name = "markupsafe", specifier = ">=2.0" }, + { name = "mongoengine", marker = "extra == 'mongoengine'", specifier = ">=0.29.0" }, + { name = "peewee", marker = "extra == 'peewee'", specifier = ">=3.14.0" }, + { name = "pillow", marker = "extra == 'images'", specifier = ">=10.0.0" }, + { name = "pymongo", marker = "extra == 'pymongo'", specifier = ">=3.10.0" }, + { name = "redis", marker = "extra == 'rediscli'", specifier = ">=4.0.0" }, + { name = "shapely", marker = "extra == 'geoalchemy'", specifier = ">=2" }, + { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=1.4" }, + { name = "sqlalchemy-citext", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=1.8.0" }, + { name = "sqlalchemy-utils", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=0.38.0" }, + { name = "tablib", marker = "extra == 'export'", specifier = ">=3.0.0" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "werkzeug", specifier = ">=2.0" }, + { name = "wtf-peewee", marker = "extra == 'peewee'", specifier = ">=3.0.4" }, + { name = "wtforms", specifier = ">=2.3" }, +] +provides-extras = ["sqlalchemy", "sqlalchemy-with-utils", "geoalchemy", "pymongo", "mongoengine", "peewee", "s3", "azure-blob-storage", "images", "export", "rediscli", "translation", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "beautifulsoup4" }, + { name = "botocore", specifier = ">=1.35" }, + { name = "flake8" }, + { name = "flask", extras = ["async"] }, + { name = "flask-admin", extras = ["all"], editable = "../../" }, + { name = "moto" }, + { name = "mypy" }, + { name = "pallets-sphinx-themes" }, + { name = "pre-commit" }, + { name = "pre-commit-uv" }, + { name = "psycopg2-binary" }, + { name = "pylint" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "sphinx" }, + { name = "sphinx-intl", specifier = ">=2.3.1" }, + { name = "sphinxcontrib-log-cabinet" }, + { name = "tox" }, + { name = "tox-uv" }, + { name = "types-beautifulsoup4" }, + { name = "types-boto3" }, + { name = "types-peewee" }, + { name = "types-pillow" }, + { name = "types-shapely" }, + { name = "types-wtforms" }, +] +docs = [ + { name = "flask-admin", extras = ["all"], editable = "../../" }, + { name = "pallets-sphinx-themes" }, + { name = "sphinx" }, + { name = "sphinx-intl", specifier = ">=2.3.1" }, + { name = "sphinxcontrib-log-cabinet" }, +] +pre-commit = [ + { name = "pre-commit" }, + { name = "pre-commit-uv" }, +] +tests = [ + { name = "beautifulsoup4" }, + { name = "botocore", specifier = ">=1.35" }, + { name = "flake8" }, + { name = "flask", extras = ["async"] }, + { name = "moto" }, + { name = "psycopg2-binary" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] +typing = [ + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "types-beautifulsoup4" }, + { name = "types-boto3" }, + { name = "types-peewee" }, + { name = "types-pillow" }, + { name = "types-shapely" }, + { name = "types-wtforms" }, +] + +[[package]] +name = "flask-sqlalchemy" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" }, + { url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" }, + { url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" }, + { url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" }, + { url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" }, + { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, + { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "redis" +version = "7.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/8f/f125feec0b958e8d22c8f0b492b30b1991d9499a4315dfde466cf4289edc/redis-7.0.1.tar.gz", hash = "sha256:c949df947dca995dc68fdf5a7863950bf6df24f8d6022394585acc98e81624f1", size = 4755322, upload-time = "2025-10-27T14:34:00.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/97/9f22a33c475cda519f20aba6babb340fb2f2254a02fb947816960d1e669a/redis-7.0.1-py3-none-any.whl", hash = "sha256:4977af3c7d67f8f0eb8b6fec0dafc9605db9343142f634041fb0235f67c0588a", size = 339938, upload-time = "2025-10-27T14:33:58.553Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/a7/e9ccfa7eecaf34c6f57d8cb0bb7cbdeeff27017cc0f5d0ca90fdde7a7c0d/sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce", size = 2137282, upload-time = "2025-10-10T15:36:10.965Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/50bc121885bdf10833a4f65ecbe9fe229a3215f4d65a58da8a181734cae3/sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985", size = 2127322, upload-time = "2025-10-10T15:36:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/a8573b7230a3ce5ee4b961a2d510d71b43872513647398e595b744344664/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0", size = 3214772, upload-time = "2025-10-10T15:34:15.09Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/c63d8adb6a7edaf8dcb6f75a2b1e9f8577960a1e489606859c4d73e7d32b/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e", size = 3214434, upload-time = "2025-10-10T15:47:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a6/243d277a4b54fae74d4797957a7320a5c210c293487f931cbe036debb697/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749", size = 3155365, upload-time = "2025-10-10T15:34:17.932Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f8/6a39516ddd75429fd4ee5a0d72e4c80639fab329b2467c75f363c2ed9751/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2", size = 3178910, upload-time = "2025-10-10T15:47:02.346Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/118355d4ad3c39d9a2f5ee4c7304a9665b3571482777357fa9920cd7a6b4/sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165", size = 2105624, upload-time = "2025-10-10T15:38:15.552Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/6ae5f9466f8aa5d0dcebfff8c9c33b98b27ce23292df3b990454b3d434fd/sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5", size = 2129240, upload-time = "2025-10-10T15:38:17.175Z" }, + { url = "https://files.pythonhosted.org/packages/e3/81/15d7c161c9ddf0900b076b55345872ed04ff1ed6a0666e5e94ab44b0163c/sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", size = 2140517, upload-time = "2025-10-10T15:36:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d5/4abd13b245c7d91bdf131d4916fd9e96a584dac74215f8b5bc945206a974/sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", size = 2130738, upload-time = "2025-10-10T15:36:16.91Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/8418969879c26522019c1025171cefbb2a8586b6789ea13254ac602986c0/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", size = 3304145, upload-time = "2025-10-10T15:34:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/94/2d/fdb9246d9d32518bda5d90f4b65030b9bf403a935cfe4c36a474846517cb/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", size = 3304511, upload-time = "2025-10-10T15:47:05.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/40f2ad1da97d5c83f6c1269664678293d3fe28e90ad17a1093b735420549/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", size = 3235161, upload-time = "2025-10-10T15:34:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/95/cb/7cf4078b46752dca917d18cf31910d4eff6076e5b513c2d66100c4293d83/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", size = 3261426, upload-time = "2025-10-10T15:47:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3b/55c09b285cb2d55bdfa711e778bdffdd0dc3ffa052b0af41f1c5d6e582fa/sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", size = 2105392, upload-time = "2025-10-10T15:38:20.051Z" }, + { url = "https://files.pythonhosted.org/packages/c7/23/907193c2f4d680aedbfbdf7bf24c13925e3c7c292e813326c1b84a0b878e/sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", size = 2130293, upload-time = "2025-10-10T15:38:21.601Z" }, + { url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" }, + { url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" }, + { url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" }, + { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, + { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +] + +[[package]] +name = "sqlalchemy-citext" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/c3/404caceaffdf0dcfa822e8b068aebc4fef328bf85af42b6cd8fdd2b2555b/sqlalchemy-citext-1.8.0.tar.gz", hash = "sha256:a1740e693a9a334e7c8f60ae731083fe75ce6c1605bb9ca6644a6f1f63b15b77", size = 3601, upload-time = "2021-03-02T18:14:03.539Z" } + +[[package]] +name = "sqlalchemy-utils" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/80/4e15fdcfc25a2226122bf316f0ebac86d840ab3fb38b38ca4cabc395865e/sqlalchemy_utils-0.42.0.tar.gz", hash = "sha256:6d1ecd3eed8b941f0faf8a531f5d5cee7cffa2598fcf8163de8c31c7a417a5e0", size = 130531, upload-time = "2025-08-30T18:43:41.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/86/21e97809b017a4ebc88971eea335130782421851b0ed8dc3ab6126b479f1/sqlalchemy_utils-0.42.0-py3-none-any.whl", hash = "sha256:c8c0b7f00f4734f6f20e9a4d06b39d79d58c8629cba50924fcaeb20e28eb4f48", size = 91744, upload-time = "2025-08-30T18:43:40.199Z" }, +] + +[[package]] +name = "tablib" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] + +[[package]] +name = "wtforms" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" }, +] diff --git a/flask_admin/static/fomanticui/css/style.css b/flask_admin/static/fomanticui/css/style.css new file mode 100644 index 0000000000..027ef526f2 --- /dev/null +++ b/flask_admin/static/fomanticui/css/style.css @@ -0,0 +1,168 @@ +/* Flask-Admin Fomantic UI Theme Core Styles */ + +.p-0 { + padding: 0 !important; +} + +.m-0 { + margin: 0 !important; +} + +.d-none { + display: none !important; +} + +.p-relative { + position: relative; +} + +.mx-auto { + margin-left: auto !important; + margin-right: auto !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.mb-3 { + margin-bottom: 1rem !important; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 2rem !important; +} + +#fixed-menu { + height: 50px; +} + +.ui.menu .item img.logo { + margin-right: 1.5em; +} + +#sidebar-menu { + position: fixed; + top: 50px; + left: 0; + border-radius: 0; + border-width: 0 1px 0 0; + box-shadow: none; + margin: 0; + width: inherit; + max-height: 100vh; + overflow: auto; + z-index: 100; + translate: -100%; + width: 280px; + min-height: 100%; + transition: translate 0.15s; + padding-bottom: 100px; +} + +html.sidebar-pref-open #sidebar-menu, +#sidebar-menu.open { + translate: 0px; +} + +#main-pusher { + padding-left: 0; + min-height: 100%; + transition: padding-left 0.15s; +} + +html.sidebar-pref-open #main-pusher, +#main-pusher.sidebar-open { + padding-left: 280px; +} + +html.sidebar-pref-open #main-container, +#main-pusher.sidebar-open #main-container { + padding: 0 20px 3em 20px; +} + +#main-container { + margin-top: 70px; + min-height: calc(100vh - 70px); + padding-bottom: 3em; + overflow-wrap: break-word; +} + +.ui.footer.segment { + padding: 5em 0em; +} + +@media only screen and (max-width: 768px) { + #sidebar-menu { + translate: -100%; + } + + html.sidebar-pref-open #sidebar-menu, + #sidebar-menu.open { + translate: 0px; + } + + #main-pusher { + padding-left: 0; + } + + #main-pusher.sidebar-open, + html.sidebar-pref-open #main-pusher { + padding-left: 0; + } + + #main-pusher.sidebar-open #main-container, + html.sidebar-pref-open #main-container { + padding: 0; + } +} + +.visually-hidden-focusable { + position: absolute; + left: -9999px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; + z-index: 1100; + background: #fff; + color: #000; + padding: 8px 16px; + border-radius: 4px; + text-decoration: none; +} + +.visually-hidden-focusable:focus { + left: 16px; + top: 8px; + width: auto; + height: auto; + outline: 2px solid #2185d0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.listing-menu-bar { + height: 100% !important; +} + +.listing-menu-bar>.button:not(:last-child) { + margin: auto 6px !important; +} + +.listing-menu-bar>.button:last-child { + margin: auto 0 auto 6px !important; +} + +form.ui.form.admin-form { + max-width: 40em; + margin-left: auto; + margin-right: auto; +} \ No newline at end of file diff --git a/flask_admin/static/fomanticui/js/actions.js b/flask_admin/static/fomanticui/js/actions.js new file mode 100644 index 0000000000..52e51076f7 --- /dev/null +++ b/flask_admin/static/fomanticui/js/actions.js @@ -0,0 +1,65 @@ +var AdminModelActions = function (actionErrorMessage, actionConfirmations) { + // batch actions helpers + this.execute = function (name) { + var selected = $('input.action-checkbox:checked').length; + + if (selected === 0) { + $.toast({ + 'class': 'warning yellow inverted', + 'message': actionErrorMessage + }); + return false; + } + + var msg = actionConfirmations[name]; + + if (!!msg) + // @TODO: change to modal: https://fomantic-ui.com/modules/modal.html + if (!confirm(msg)) + return false; + + // Update hidden form and submit it + var form = $('#action_form'); + $('#action', form).val(name); + + $('input.action-checkbox', form).remove(); + $('input.action-checkbox:checked').each(function () { + form.append($(this).clone()); + }); + form.submit(); + + return false; + }; + + $(function () { + $('.action-rowtoggle').change(function () { + $('input.action-checkbox').prop('checked', this.checked); + $('input.action-checkbox').closest('tr').toggleClass('violet', this.checked); + }); + }); + + $(function () { + $('input.action-checkbox').change(function () { + $(this).closest('tr').toggleClass('violet', this.checked); + }); + }); + $(function () { + var inputs = $('input.action-checkbox'); + inputs.change(function () { + var allInputsChecked = true; + for (var i = 0; i < inputs.length; i++) { + if (!inputs[i].checked) { + allInputsChecked = false; + break; + } + } + $('.action-rowtoggle').attr('checked', allInputsChecked); + }); + }); +}; +var modelActions = new AdminModelActions(JSON.parse($('#message-data').text()), JSON.parse($('#actions-confirmation-data').text())); + + + + + diff --git a/flask_admin/static/fomanticui/js/details_filter.js b/flask_admin/static/fomanticui/js/details_filter.js new file mode 100644 index 0000000000..216b146be2 --- /dev/null +++ b/flask_admin/static/fomanticui/js/details_filter.js @@ -0,0 +1,10 @@ +// filters the details table based on input +$(document).ready(function () { + $('#fa_filter').keyup(function () { + var rex = new RegExp($(this).val(), 'i'); + $('.searchable tr').hide(); + $('.searchable tr').filter(function () { + return rex.test($(this).text()); + }).show(); + }); +}); diff --git a/flask_admin/static/fomanticui/js/filters.js b/flask_admin/static/fomanticui/js/filters.js new file mode 100644 index 0000000000..15efeb749d --- /dev/null +++ b/flask_admin/static/fomanticui/js/filters.js @@ -0,0 +1,377 @@ +/* Fomantic-UI admin list filters + * -------------------------------------------------------------- + * This replaces the old Select2-based implementation with native + * Fomantic-UI dropdowns + the existing Flask-Admin form helpers. + * -------------------------------------------------------------- */ +var AdminFilters = function (element, filtersElement, filtersFormElement, filterGroups, activeFilters) { + var $root = $(element); + var $container = $('.filters', $root); + var $filtersFormElement = $(filtersFormElement); + + var lastCount = 0; + + /* ------------------------------------------------------------ + * Helpers + * ---------------------------------------------------------- */ + function getCount(name) { + var idx = name.indexOf('_'); + if (idx === -1) { return 0; } + return parseInt(name.substr(3, idx - 3), 10); + } + + function makeName(arg) { + var name = 'flt' + lastCount + '_' + arg; + lastCount += 1; + return name; + } + + function updateFiltersCount() { + $('.filters-count').text($('.filters > tr').length || ''); + } + + function isCalendar(type) { + return [ + 'datepicker', 'datetimepicker', 'timepicker', + 'daterangepicker', 'datetimerangepicker', 'timerangepicker' + ].includes(type); + } + function isCalendarRange(type) { + return [ + 'daterangepicker', 'datetimerangepicker', 'timerangepicker' + ].includes(type); + } + + function ensureContainerExists() { + if ($container.length === 0) { + $container = $('').appendTo($root); + } + } + + /* ------------------------------------------------------------ + * Remove filter row + * ---------------------------------------------------------- */ + function removeFilter() { + $(this).closest('tr').remove(); + + if ($('.filters >tr').length === 0) { + $('#apply-filters-btn', $root).addClass('disabled'); + $('.filters').remove(); // remove empty tbody + $container = $(); // reset reference + } else { + $('#apply-filters-btn', $root).removeClass('disabled'); + } + updateFiltersCount(); + return false; + } + + /* ------------------------------------------------------------ + * Build / style the value field + * ---------------------------------------------------------- */ + function createFilterInput($td, filterValue, filter) { + let $field; + + /* ---------- CALENDAR TYPES --------------------------------- */ + if (isCalendar(filter.type)) { + /* single date / time input */ + if (!isCalendarRange(filter.type)) { + const $input = $('') + .attr('name', makeName(filter.arg)) + .val(filterValue || ''); + + $field = $('
') + .append( + $('
') + .append('') + .append($input) + ); + } + /* ---------------- RANGE: start + end --------------------- */ + else { + const name = makeName(filter.arg); + const $hidden = $('') + .attr('name', name) + .val(filterValue || ''); + + const $startInput = $(''); + const $endInput = $(''); + + const [startValue, endValue] = (filterValue || '').split(' to '); + $startInput.val(startValue || ''); + $endInput.val(endValue || ''); + + const $startCal = $('
') + .append($('
') + .append($startInput)); + const $endCal = $('
') + .append($('
') + .append($endInput)); + + // wrapper that faForm will receive + $field = $('
') + .append( + $('
').append($startCal) + ) + .append( + $('
').append($endCal) + ) + .append($hidden); + + /* keep hidden input updated */ + function syncHidden() { + const start = $startInput.val(); + const end = $endInput.val(); + $hidden.val(start && end ? `${start} to ${end}` : ''); + } + $startInput.add($endInput).on('change input', syncHidden); + } + } + + /* Handle select2-tags type filters and filters with options */ + else if (filter.type === 'select2-tags' || filter.options) { + /* Case 1: select2-tags type filter */ + if (filter.type === 'select2-tags') { + // Parse selected values from comma-separated string + let selectedValues = []; + if (filterValue) { + selectedValues = filterValue.split(',').map(function (s) { return s.trim(); }); + } + + // Create multi-select dropdown field + $field = $('') + .attr('name', makeName(filter.arg)); + + // Add search capability for dropdowns with many options + if (filter.options.length > 8) { + $field.addClass('search'); + } + + // Add options to dropdown + $(filter.options).each(function () { + var opt = $('