Skip to content
Merged

Htmx #42

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion blog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ def create_app(init_admin: bool = False) -> Flask:

configure_extensions(app)
if init_admin or env != "testing":
print("init admin")
create_admin(admin_ext)
app.register_blueprint(post)
app.register_blueprint(tags)
Expand Down
1 change: 1 addition & 0 deletions blog/category/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Category(db.Model):
title: Mapped[str | None]
alias: Mapped[str | None]
template: Mapped[str | None]
page: Mapped[bool | None]

posts: Mapped[list["Post"]] = relationship("Post", back_populates="category")

Expand Down
7 changes: 0 additions & 7 deletions blog/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ class Config(object):
YANDEX_VERIFICATION: str | None = environ.get("YANDEX_VERIFICATION", None)
YANDEX_METRIKA: str = environ.get("YANDEX_METRIKA", "76938046")

PAGE_CATEGORY: list[int] = [
int(c) for c in environ.get("PAGE_CATEGORY", "0").split(",")
]


class DevelopmentConfig(Config):
DEBUG: bool = True
Expand All @@ -30,9 +26,6 @@ class DevelopmentConfig(Config):
class TestingConfig(Config):
TESTING: bool = True
SQLALCHEMY_DATABASE_URI: str = "sqlite:///:memory:"
PAGE_CATEGORY: list[int] = [
1,
]


class ProductionConfig(Config):
Expand Down
13 changes: 1 addition & 12 deletions blog/config_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import logging
import os
from typing import Any, cast
from typing import Any

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,17 +61,6 @@ def validate_config(config: dict[str, Any]) -> list[str]: # pyright: ignore[rep
+ "Supported schemes: sqlite://, postgresql://, mysql://, oracle://"
)

# Validate PAGE_CATEGORY
page_category = config.get("PAGE_CATEGORY")
print(page_category, type(page_category))
if page_category is not None:
if not isinstance(page_category, list):
raise ConfigValidationError("PAGE_CATEGORY must be a list of integers")
for item in cast("list[Any]", page_category): # pyright: ignore[reportExplicitAny]
if not isinstance(item, int):
raise ConfigValidationError("PAGE_CATEGORY must contain only integers")

# Validate PORT
port = config.get("PORT")
if port:
try:
Expand Down
1 change: 1 addition & 0 deletions blog/domain/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Category:
title: str = ""
alias: str = ""
template: str | None = None
page: bool | None = False

@override
def __str__(self):
Expand Down
1 change: 1 addition & 0 deletions blog/domain/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Post:
createdon: datetime.datetime | None = None
publishedon: datetime.datetime | None = None
category_id: int | None = None
is_page: bool = False
user_id: int | None = None

def __post_init__(self):
Expand Down
12 changes: 11 additions & 1 deletion blog/infrastructure/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@
is separated from domain models.
"""

from sqlalchemy import Table, Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy import (
Table,
Column,
Integer,
String,
Text,
DateTime,
ForeignKey,
Boolean,
)
from sqlalchemy.sql.schema import MetaData


Expand Down Expand Up @@ -47,6 +56,7 @@ def get_categories_table(metadata: MetaData) -> Table:
Column("title", String(255)),
Column("alias", String(255), unique=True),
Column("template", String(255), nullable=True),
Column("page", Boolean(), nullable=True, default=False),
extend_existing=True,
)

Expand Down
14 changes: 4 additions & 10 deletions blog/post/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import datetime
from typing import cast, ParamSpec, TypeVar
from typing import ParamSpec, TypeVar

import markdown
from flask import (
Blueprint,
Response,
current_app,
jsonify,
make_response,
render_template,
Expand Down Expand Up @@ -42,9 +41,8 @@ def posts(**kwargs: str) -> Response | str:
@post.route("/hx/pages")
@cache.cached(timeout=50) # pyright: ignore[reportUntypedFunctionDecorator]
def pages_hx() -> Response | str:
page_category = cast("list[int]", current_app.config["PAGE_CATEGORY"])
post_service = ServiceFactory.create_post_service()
pages = post_service.get_page_posts(page_category)
pages = post_service.get_page_posts()
return render_template("pages.htmx", pages=pages)


Expand All @@ -69,11 +67,9 @@ def view(alias: str | None = None, **kwargs: str) -> Response | str:
abort(404)

# For page categories, we need to check if it's a page or a regular post
page_categories = cast("list[int]", current_app.config["PAGE_CATEGORY"])
is_page = post.category_id is not None and post.category_id in page_categories
is_published = post.publishedon is not None

if not (is_published or is_page):
if not (is_published or post.is_page):
abort(404)

# Get tags for the post
Expand All @@ -91,10 +87,8 @@ def view(alias: str | None = None, **kwargs: str) -> Response | str:

@flask_sitemap.register_generator
def site_map_gen():
page_category = cast("list[int]", current_app.config["PAGE_CATEGORY"])

post_service = ServiceFactory.create_post_service()
pages = post_service.get_page_posts(page_category)
pages = post_service.get_page_posts()
for page in pages:
yield url_for("post.view", alias=page.alias)

Expand Down
1 change: 1 addition & 0 deletions blog/repos/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def create(self, entity: CategoryDomain) -> CategoryDomain:
category_orm = CategoryORM()
category_orm.title = entity.title
category_orm.alias = entity.alias
category_orm.page = entity.page
# Handle the case where template might be None
if entity.template is not None:
category_orm.template = entity.template
Expand Down
22 changes: 18 additions & 4 deletions blog/repos/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sqlalchemy as sa
from typing import Any, override

from blog.category.models import Category as CategoryOrm
from blog.extensions import db
from blog.post.models import Post as PostORM
from blog.tags.models import Tag as TagORM
Expand Down Expand Up @@ -66,7 +67,11 @@ def get_by_id(self, id: int) -> PostDomain | None:
return None

def get_by_alias(self, alias: str) -> PostDomain | None:
stmt = sa.select(PostORM).where(PostORM.alias == alias)
stmt = (
sa.select(PostORM)
.where(PostORM.alias == alias)
.join(CategoryOrm, PostORM.category_id == CategoryOrm.id, isouter=True)
)
post_orm = self.session.scalar(stmt)
if post_orm:
return self._to_domain_model(post_orm)
Expand Down Expand Up @@ -94,15 +99,23 @@ def get_all_published_content(self) -> list[PostDomain]:
"""Get all published content including posts and pages."""
stmt = (
sa.select(PostORM)
.join(CategoryOrm, PostORM.category_id == CategoryOrm.id, isouter=True)
.where(PostORM.publishedon.isnot(None))
.where(PostORM.category_id.isnot(1))
.where(CategoryOrm.page.isnot(True))
.order_by(PostORM.publishedon.desc())
)
posts_orm = list(self.session.scalars(stmt).all())
return [self._to_domain_model(post_orm) for post_orm in posts_orm]

def get_page_posts(self, page_category_ids: list[int]) -> list[PostDomain]:
stmt = sa.select(PostORM).where(PostORM.category_id.in_(page_category_ids))
def get_page_posts(self) -> list[PostDomain]:
stmt = (
sa.select(PostORM)
.join_from(
PostORM,
CategoryOrm,
)
.where(CategoryOrm.page) # pyright: ignore[reportArgumentType]
)
posts_orm = list(self.session.scalars(stmt).all())
return [self._to_domain_model(post_orm) for post_orm in posts_orm]

Expand Down Expand Up @@ -167,5 +180,6 @@ def _to_domain_model(self, post_orm: PostORM) -> PostDomain:
createdon=post_orm.createdon,
publishedon=post_orm.publishedon,
category_id=post_orm.category_id,
is_page=bool(post_orm.category.page) if post_orm.category_id else False,
user_id=post_orm.user_id,
)
4 changes: 2 additions & 2 deletions blog/services/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def get_published_posts(self) -> list[Post]:
def get_all_published_content(self) -> list[Post]:
return self.post_repository.get_all_published_content()

def get_page_posts(self, page_category_ids: list[int]) -> list[Post]:
return self.post_repository.get_page_posts(page_category_ids)
def get_page_posts(self) -> list[Post]:
return self.post_repository.get_page_posts()

def get_posts_by_tag(self, tag_id: int) -> list[Post]:
"""Get all posts associated with a specific tag."""
Expand Down
5 changes: 1 addition & 4 deletions config.example.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ export PORT="5555"
# Yandex Metrika ID (optional)
# export YANDEX_METRIKA="your-yandex-metrika-id"

# Page categories (comma-separated list of integers)
export PAGE_CATEGORY="0"

# Cache configuration (optional)
# export CACHE_TYPE="SimpleCache"
# export CACHE_DEFAULT_TIMEOUT="300"
# export CACHE_DEFAULT_TIMEOUT="300"
33 changes: 33 additions & 0 deletions migrations/versions/b7b6e253e6fb_category_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""empty message

Revision ID: b7b6e253e6fb
Revises: eead387d732e
Create Date: 2025-11-09 20:58:03.081802

"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "b7b6e253e6fb"
down_revision = "eead387d732e"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("categories", schema=None) as batch_op:
batch_op.add_column(sa.Column("page", sa.Boolean(), nullable=True))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("categories", schema=None) as batch_op:
batch_op.drop_column("page")

# ### end Alembic commands ###
93 changes: 93 additions & 0 deletions migrations/versions/eead387d732e_backward.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""empty message

Revision ID: eead387d732e
Revises: f00000000002
Create Date: 2025-11-09 20:49:44.655909

"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "eead387d732e"
down_revision = "f00000000002"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("categories", schema=None) as batch_op:
batch_op.alter_column(
"title", existing_type=sa.VARCHAR(length=255), nullable=True
)
batch_op.alter_column(
"alias", existing_type=sa.VARCHAR(length=255), nullable=True
)

with op.batch_alter_table("posts", schema=None) as batch_op:
batch_op.alter_column("createdon", existing_type=sa.DATETIME(), nullable=True)

with op.batch_alter_table("tags", schema=None) as batch_op:
batch_op.alter_column(
"title", existing_type=sa.VARCHAR(length=255), nullable=True
)
batch_op.alter_column(
"alias", existing_type=sa.VARCHAR(length=255), nullable=True
)

with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"authenticated",
existing_type=sa.BOOLEAN(),
type_=sa.Integer(),
existing_nullable=True,
)
batch_op.alter_column(
"createdon",
existing_type=sa.DATETIME(),
nullable=True,
existing_server_default=sa.text("(CURRENT_TIMESTAMP)"),
)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"createdon",
existing_type=sa.DATETIME(),
nullable=False,
existing_server_default=sa.text("(CURRENT_TIMESTAMP)"),
)
batch_op.alter_column(
"authenticated",
existing_type=sa.Integer(),
type_=sa.BOOLEAN(),
existing_nullable=True,
)

with op.batch_alter_table("tags", schema=None) as batch_op:
batch_op.alter_column(
"alias", existing_type=sa.VARCHAR(length=255), nullable=False
)
batch_op.alter_column(
"title", existing_type=sa.VARCHAR(length=255), nullable=False
)

with op.batch_alter_table("posts", schema=None) as batch_op:
batch_op.alter_column("createdon", existing_type=sa.DATETIME(), nullable=False)

with op.batch_alter_table("categories", schema=None) as batch_op:
batch_op.alter_column(
"alias", existing_type=sa.VARCHAR(length=255), nullable=False
)
batch_op.alter_column(
"title", existing_type=sa.VARCHAR(length=255), nullable=False
)

# ### end Alembic commands ###
1 change: 0 additions & 1 deletion tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ def temp_user(admin_app, auth_adapter, user_service):

# Load the user using the adapter
loaded_user = auth_adapter.load_user(created_user.id)
print(loaded_user, type(loaded_user))
assert loaded_user is not None

return user_orm
Expand Down
Loading