Improve topic sorting: add popular sorting by publications and authors count
This commit is contained in:
@@ -2,7 +2,7 @@ import time
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class ShoutCollection(Base):
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import enum
|
||||
import time
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Text, distinct, func
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text, distinct, func
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from services.db import Base
|
||||
from services.db import BaseModel
|
||||
|
||||
|
||||
class CommunityRole(enum.Enum):
|
||||
@@ -14,28 +15,36 @@ class CommunityRole(enum.Enum):
|
||||
ARTIST = "artist" # + can be credited as featured artist
|
||||
EXPERT = "expert" # + can add proof or disproof to shouts, can manage topics
|
||||
EDITOR = "editor" # + can manage topics, comments and community settings
|
||||
ADMIN = "admin"
|
||||
|
||||
@classmethod
|
||||
def as_string_array(cls, roles):
|
||||
return [role.value for role in roles]
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value: str) -> "CommunityRole":
|
||||
return cls(value)
|
||||
|
||||
class CommunityFollower(Base):
|
||||
__tablename__ = "community_author"
|
||||
|
||||
author = Column(ForeignKey("author.id"), primary_key=True)
|
||||
class CommunityFollower(BaseModel):
|
||||
__tablename__ = "community_follower"
|
||||
|
||||
community = Column(ForeignKey("community.id"), primary_key=True)
|
||||
joined_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
roles = Column(Text, nullable=True, comment="Roles (comma-separated)")
|
||||
follower = Column(ForeignKey("author.id"), primary_key=True)
|
||||
roles = Column(String, nullable=True)
|
||||
|
||||
def set_roles(self, roles):
|
||||
self.roles = CommunityRole.as_string_array(roles)
|
||||
def __init__(self, community: int, follower: int, roles: list[str] | None = None) -> None:
|
||||
self.community = community # type: ignore[assignment]
|
||||
self.follower = follower # type: ignore[assignment]
|
||||
if roles:
|
||||
self.roles = ",".join(roles) # type: ignore[assignment]
|
||||
|
||||
def get_roles(self):
|
||||
return [CommunityRole(role) for role in self.roles]
|
||||
def get_roles(self) -> list[CommunityRole]:
|
||||
roles_str = getattr(self, "roles", "")
|
||||
return [CommunityRole(role) for role in roles_str.split(",")] if roles_str else []
|
||||
|
||||
|
||||
class Community(Base):
|
||||
class Community(BaseModel):
|
||||
__tablename__ = "community"
|
||||
|
||||
name = Column(String, nullable=False)
|
||||
@@ -44,6 +53,12 @@ class Community(Base):
|
||||
pic = Column(String, nullable=False, default="")
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
created_by = Column(ForeignKey("author.id"), nullable=False)
|
||||
settings = Column(JSON, nullable=True)
|
||||
updated_at = Column(Integer, nullable=True)
|
||||
deleted_at = Column(Integer, nullable=True)
|
||||
private = Column(Boolean, default=False)
|
||||
|
||||
followers = relationship("Author", secondary="community_follower")
|
||||
|
||||
@hybrid_property
|
||||
def stat(self):
|
||||
@@ -54,12 +69,39 @@ class Community(Base):
|
||||
return self.roles.split(",") if self.roles else []
|
||||
|
||||
@role_list.setter
|
||||
def role_list(self, value):
|
||||
self.roles = ",".join(value) if value else None
|
||||
def role_list(self, value) -> None:
|
||||
self.roles = ",".join(value) if value else None # type: ignore[assignment]
|
||||
|
||||
def is_followed_by(self, author_id: int) -> bool:
|
||||
# Check if the author follows this community
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
follower = (
|
||||
session.query(CommunityFollower)
|
||||
.filter(CommunityFollower.community == self.id, CommunityFollower.follower == author_id)
|
||||
.first()
|
||||
)
|
||||
return follower is not None
|
||||
|
||||
def get_role(self, author_id: int) -> CommunityRole | None:
|
||||
# Get the role of the author in this community
|
||||
from services.db import local_session
|
||||
|
||||
with local_session() as session:
|
||||
follower = (
|
||||
session.query(CommunityFollower)
|
||||
.filter(CommunityFollower.community == self.id, CommunityFollower.follower == author_id)
|
||||
.first()
|
||||
)
|
||||
if follower and follower.roles:
|
||||
roles = follower.roles.split(",")
|
||||
return CommunityRole.from_string(roles[0]) if roles else None
|
||||
return None
|
||||
|
||||
|
||||
class CommunityStats:
|
||||
def __init__(self, community):
|
||||
def __init__(self, community) -> None:
|
||||
self.community = community
|
||||
|
||||
@property
|
||||
@@ -71,7 +113,7 @@ class CommunityStats:
|
||||
@property
|
||||
def followers(self):
|
||||
return (
|
||||
self.community.session.query(func.count(CommunityFollower.author))
|
||||
self.community.session.query(func.count(CommunityFollower.follower))
|
||||
.filter(CommunityFollower.community == self.community.id)
|
||||
.scalar()
|
||||
)
|
||||
@@ -93,7 +135,7 @@ class CommunityStats:
|
||||
)
|
||||
|
||||
|
||||
class CommunityAuthor(Base):
|
||||
class CommunityAuthor(BaseModel):
|
||||
__tablename__ = "community_author"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
@@ -106,5 +148,5 @@ class CommunityAuthor(Base):
|
||||
return self.roles.split(",") if self.roles else []
|
||||
|
||||
@role_list.setter
|
||||
def role_list(self, value):
|
||||
self.roles = ",".join(value) if value else None
|
||||
def role_list(self, value) -> None:
|
||||
self.roles = ",".join(value) if value else None # type: ignore[assignment]
|
||||
|
91
orm/draft.py
91
orm/draft.py
@@ -5,7 +5,7 @@ from sqlalchemy.orm import relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from orm.topic import Topic
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class DraftTopic(Base):
|
||||
@@ -29,76 +29,27 @@ class DraftAuthor(Base):
|
||||
class Draft(Base):
|
||||
__tablename__ = "draft"
|
||||
# required
|
||||
created_at: int = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
# Колонки для связей с автором
|
||||
created_by: int = Column("created_by", ForeignKey("author.id"), nullable=False)
|
||||
community: int = Column("community", ForeignKey("community.id"), nullable=False, default=1)
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
created_by = Column(ForeignKey("author.id"), nullable=False)
|
||||
community = Column(ForeignKey("community.id"), nullable=False, default=1)
|
||||
|
||||
# optional
|
||||
layout: str = Column(String, nullable=True, default="article")
|
||||
slug: str = Column(String, unique=True)
|
||||
title: str = Column(String, nullable=True)
|
||||
subtitle: str | None = Column(String, nullable=True)
|
||||
lead: str | None = Column(String, nullable=True)
|
||||
body: str = Column(String, nullable=False, comment="Body")
|
||||
media: dict | None = Column(JSON, nullable=True)
|
||||
cover: str | None = Column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption: str | None = Column(String, nullable=True, comment="Cover image alt caption")
|
||||
lang: str = Column(String, nullable=False, default="ru", comment="Language")
|
||||
seo: str | None = Column(String, nullable=True) # JSON
|
||||
layout = Column(String, nullable=True, default="article")
|
||||
slug = Column(String, unique=True)
|
||||
title = Column(String, nullable=True)
|
||||
subtitle = Column(String, nullable=True)
|
||||
lead = Column(String, nullable=True)
|
||||
body = Column(String, nullable=False, comment="Body")
|
||||
media = Column(JSON, nullable=True)
|
||||
cover = Column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption = Column(String, nullable=True, comment="Cover image alt caption")
|
||||
lang = Column(String, nullable=False, default="ru", comment="Language")
|
||||
seo = Column(String, nullable=True) # JSON
|
||||
|
||||
# auto
|
||||
updated_at: int | None = Column(Integer, nullable=True, index=True)
|
||||
deleted_at: int | None = Column(Integer, nullable=True, index=True)
|
||||
updated_by: int | None = Column("updated_by", ForeignKey("author.id"), nullable=True)
|
||||
deleted_by: int | None = Column("deleted_by", ForeignKey("author.id"), nullable=True)
|
||||
|
||||
# --- Relationships ---
|
||||
# Только many-to-many связи через вспомогательные таблицы
|
||||
authors = relationship(Author, secondary="draft_author", lazy="select")
|
||||
topics = relationship(Topic, secondary="draft_topic", lazy="select")
|
||||
|
||||
# Связь с Community (если нужна как объект, а не ID)
|
||||
# community = relationship("Community", foreign_keys=[community_id], lazy="joined")
|
||||
# Пока оставляем community_id как ID
|
||||
|
||||
# Связь с публикацией (один-к-одному или один-к-нулю)
|
||||
# Загружается через joinedload в резолвере
|
||||
publication = relationship(
|
||||
"Shout",
|
||||
primaryjoin="Draft.id == Shout.draft",
|
||||
foreign_keys="Shout.draft",
|
||||
uselist=False,
|
||||
lazy="noload", # Не грузим по умолчанию, только через options
|
||||
viewonly=True, # Указываем, что это связь только для чтения
|
||||
)
|
||||
|
||||
def dict(self):
|
||||
"""
|
||||
Сериализует объект Draft в словарь.
|
||||
Гарантирует, что поля topics и authors всегда будут списками.
|
||||
"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"created_at": self.created_at,
|
||||
"created_by": self.created_by,
|
||||
"community": self.community,
|
||||
"layout": self.layout,
|
||||
"slug": self.slug,
|
||||
"title": self.title,
|
||||
"subtitle": self.subtitle,
|
||||
"lead": self.lead,
|
||||
"body": self.body,
|
||||
"media": self.media or [],
|
||||
"cover": self.cover,
|
||||
"cover_caption": self.cover_caption,
|
||||
"lang": self.lang,
|
||||
"seo": self.seo,
|
||||
"updated_at": self.updated_at,
|
||||
"deleted_at": self.deleted_at,
|
||||
"updated_by": self.updated_by,
|
||||
"deleted_by": self.deleted_by,
|
||||
# Гарантируем, что topics и authors всегда будут списками
|
||||
"topics": [topic.dict() for topic in (self.topics or [])],
|
||||
"authors": [author.dict() for author in (self.authors or [])],
|
||||
}
|
||||
updated_at = Column(Integer, nullable=True, index=True)
|
||||
deleted_at = Column(Integer, nullable=True, index=True)
|
||||
updated_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
deleted_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
authors = relationship(Author, secondary="draft_author")
|
||||
topics = relationship(Topic, secondary="draft_topic")
|
||||
|
@@ -3,7 +3,7 @@ import enum
|
||||
from sqlalchemy import Column, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class InviteStatus(enum.Enum):
|
||||
@@ -29,7 +29,7 @@ class Invite(Base):
|
||||
shout = relationship("Shout")
|
||||
|
||||
def set_status(self, status: InviteStatus):
|
||||
self.status = status.value
|
||||
self.status = status.value # type: ignore[assignment]
|
||||
|
||||
def get_status(self) -> InviteStatus:
|
||||
return InviteStatus.from_string(self.status)
|
||||
|
@@ -5,7 +5,7 @@ from sqlalchemy import JSON, Column, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from auth.orm import Author
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class NotificationEntity(enum.Enum):
|
||||
@@ -51,13 +51,13 @@ class Notification(Base):
|
||||
seen = relationship(Author, secondary="notification_seen")
|
||||
|
||||
def set_entity(self, entity: NotificationEntity):
|
||||
self.entity = entity.value
|
||||
self.entity = entity.value # type: ignore[assignment]
|
||||
|
||||
def get_entity(self) -> NotificationEntity:
|
||||
return NotificationEntity.from_string(self.entity)
|
||||
|
||||
def set_action(self, action: NotificationAction):
|
||||
self.action = action.value
|
||||
self.action = action.value # type: ignore[assignment]
|
||||
|
||||
def get_action(self) -> NotificationAction:
|
||||
return NotificationAction.from_string(self.action)
|
||||
|
@@ -3,7 +3,7 @@ from enum import Enum as Enumeration
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class ReactionKind(Enumeration):
|
||||
|
77
orm/shout.py
77
orm/shout.py
@@ -6,7 +6,7 @@ from sqlalchemy.orm import relationship
|
||||
from auth.orm import Author
|
||||
from orm.reaction import Reaction
|
||||
from orm.topic import Topic
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class ShoutTopic(Base):
|
||||
@@ -71,70 +71,41 @@ class ShoutAuthor(Base):
|
||||
class Shout(Base):
|
||||
"""
|
||||
Публикация в системе.
|
||||
|
||||
Attributes:
|
||||
body (str)
|
||||
slug (str)
|
||||
cover (str) : "Cover image url"
|
||||
cover_caption (str) : "Cover image alt caption"
|
||||
lead (str)
|
||||
title (str)
|
||||
subtitle (str)
|
||||
layout (str)
|
||||
media (dict)
|
||||
authors (list[Author])
|
||||
topics (list[Topic])
|
||||
reactions (list[Reaction])
|
||||
lang (str)
|
||||
version_of (int)
|
||||
oid (str)
|
||||
seo (str) : JSON
|
||||
draft (int)
|
||||
created_at (int)
|
||||
updated_at (int)
|
||||
published_at (int)
|
||||
featured_at (int)
|
||||
deleted_at (int)
|
||||
created_by (int)
|
||||
updated_by (int)
|
||||
deleted_by (int)
|
||||
community (int)
|
||||
"""
|
||||
|
||||
__tablename__ = "shout"
|
||||
|
||||
created_at: int = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
updated_at: int | None = Column(Integer, nullable=True, index=True)
|
||||
published_at: int | None = Column(Integer, nullable=True, index=True)
|
||||
featured_at: int | None = Column(Integer, nullable=True, index=True)
|
||||
deleted_at: int | None = Column(Integer, nullable=True, index=True)
|
||||
created_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
|
||||
updated_at = Column(Integer, nullable=True, index=True)
|
||||
published_at = Column(Integer, nullable=True, index=True)
|
||||
featured_at = Column(Integer, nullable=True, index=True)
|
||||
deleted_at = Column(Integer, nullable=True, index=True)
|
||||
|
||||
created_by: int = Column(ForeignKey("author.id"), nullable=False)
|
||||
updated_by: int | None = Column(ForeignKey("author.id"), nullable=True)
|
||||
deleted_by: int | None = Column(ForeignKey("author.id"), nullable=True)
|
||||
community: int = Column(ForeignKey("community.id"), nullable=False)
|
||||
created_by = Column(ForeignKey("author.id"), nullable=False)
|
||||
updated_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
deleted_by = Column(ForeignKey("author.id"), nullable=True)
|
||||
community = Column(ForeignKey("community.id"), nullable=False)
|
||||
|
||||
body: str = Column(String, nullable=False, comment="Body")
|
||||
slug: str = Column(String, unique=True)
|
||||
cover: str | None = Column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption: str | None = Column(String, nullable=True, comment="Cover image alt caption")
|
||||
lead: str | None = Column(String, nullable=True)
|
||||
title: str = Column(String, nullable=False)
|
||||
subtitle: str | None = Column(String, nullable=True)
|
||||
layout: str = Column(String, nullable=False, default="article")
|
||||
media: dict | None = Column(JSON, nullable=True)
|
||||
body = Column(String, nullable=False, comment="Body")
|
||||
slug = Column(String, unique=True)
|
||||
cover = Column(String, nullable=True, comment="Cover image url")
|
||||
cover_caption = Column(String, nullable=True, comment="Cover image alt caption")
|
||||
lead = Column(String, nullable=True)
|
||||
title = Column(String, nullable=False)
|
||||
subtitle = Column(String, nullable=True)
|
||||
layout = Column(String, nullable=False, default="article")
|
||||
media = Column(JSON, nullable=True)
|
||||
|
||||
authors = relationship(Author, secondary="shout_author")
|
||||
topics = relationship(Topic, secondary="shout_topic")
|
||||
reactions = relationship(Reaction)
|
||||
|
||||
lang: str = Column(String, nullable=False, default="ru", comment="Language")
|
||||
version_of: int | None = Column(ForeignKey("shout.id"), nullable=True)
|
||||
oid: str | None = Column(String, nullable=True)
|
||||
lang = Column(String, nullable=False, default="ru", comment="Language")
|
||||
version_of = Column(ForeignKey("shout.id"), nullable=True)
|
||||
oid = Column(String, nullable=True)
|
||||
seo = Column(String, nullable=True) # JSON
|
||||
|
||||
seo: str | None = Column(String, nullable=True) # JSON
|
||||
|
||||
draft: int | None = Column(ForeignKey("draft.id"), nullable=True)
|
||||
draft = Column(ForeignKey("draft.id"), nullable=True)
|
||||
|
||||
# Определяем индексы
|
||||
__table_args__ = (
|
||||
|
@@ -2,7 +2,7 @@ import time
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String
|
||||
|
||||
from services.db import Base
|
||||
from services.db import BaseModel as Base
|
||||
|
||||
|
||||
class TopicFollower(Base):
|
||||
|
Reference in New Issue
Block a user