Skip to content
Closed
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
7 changes: 1 addition & 6 deletions chafan_core/app/crud/crud_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@
from chafan_core.app.schemas.security import IntlPhoneNumber
from chafan_core.app.schemas.user import UserCreate, UserUpdate
from chafan_core.app.security import get_password_hash, verify_password
from chafan_core.utils.validators import StrippedNonEmptyBasicStr


class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
return db.query(User).filter_by(email=email).first()
Expand Down Expand Up @@ -66,9 +63,7 @@ def get_all_active_users(self, db: Session) -> List[User]:
# 2025-Dec-11 This function is synchronized for now. We define it as async to facilitate further improvement
async def create(self, db: Session, *, obj_in: UserCreate) -> User:
if obj_in.handle is None:
handle = StrippedNonEmptyBasicStr(
self._generate_handle(db, obj_in.email.split("@")[0])
)
handle = self._generate_handle(db, obj_in.email.split("@")[0])
else:
handle = obj_in.handle
initial_coins = 0
Expand Down
4 changes: 1 addition & 3 deletions chafan_core/app/materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@
unknown_user_handle,
unknown_user_uuid,
)
from chafan_core.utils.validators import StrippedNonEmptyBasicStr

_ANONYMOUS_USER_PREVIEW = schemas.UserPreview(
uuid=unknown_user_uuid,
handle=StrippedNonEmptyBasicStr(unknown_user_handle),
handle=unknown_user_handle,
full_name=unknown_user_full_name,
)

Expand Down
95 changes: 60 additions & 35 deletions chafan_core/app/models/answer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import TYPE_CHECKING, List
import datetime
from typing import TYPE_CHECKING, Any, List, Optional

from sqlalchemy import (
CHAR,
Expand All @@ -12,7 +13,7 @@
Table,
UniqueConstraint,
)
from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm import Mapped, backref, relationship
from sqlalchemy.sql.sqltypes import JSON, Enum

from chafan_core.db.base_class import Base
Expand All @@ -29,11 +30,13 @@ class Answer_Upvotes(Base):
PrimaryKeyConstraint("answer_id", "voter_id"),
)

cancelled = Column(Boolean, server_default="false", default=False, nullable=False)
answer_id = Column(Integer, ForeignKey("answer.id"), index=True)
answer = relationship("Answer", foreign_keys=[answer_id])
voter_id = Column(Integer, ForeignKey("user.id"), index=True)
voter = relationship("User", foreign_keys=[voter_id])
cancelled: Mapped[bool] = Column(
Boolean, server_default="false", default=False, nullable=False
)
answer_id: Mapped[int] = Column(Integer, ForeignKey("answer.id"), index=True)
answer: Mapped["Answer"] = relationship("Answer", foreign_keys=[answer_id])
voter_id: Mapped[int] = Column(Integer, ForeignKey("user.id"), index=True)
voter: Mapped["User"] = relationship("User", foreign_keys=[voter_id])


answer_contributors = Table(
Expand All @@ -45,57 +48,77 @@ class Answer_Upvotes(Base):


class Answer(Base):
id = Column(Integer, primary_key=True, index=True)
uuid = Column(CHAR(length=UUID_LENGTH), index=True, unique=True, nullable=False)
author_id = Column(Integer, ForeignKey("user.id"), nullable=False, index=True)
author: "User" = relationship("User", back_populates="answers") # type: ignore
site_id = Column(Integer, ForeignKey("site.id"), nullable=False, index=True)
site: "Site" = relationship("Site", back_populates="answers") # type: ignore
question_id = Column(Integer, ForeignKey("question.id"), nullable=False, index=True)
question: "Question" = relationship("Question", back_populates="answers") # type: ignore
id: Mapped[int] = Column(Integer, primary_key=True, index=True)
uuid: Mapped[str] = Column(
CHAR(length=UUID_LENGTH), index=True, unique=True, nullable=False
)
author_id: Mapped[int] = Column(
Integer, ForeignKey("user.id"), nullable=False, index=True
)
author: Mapped["User"] = relationship("User", back_populates="answers")
site_id: Mapped[int] = Column(
Integer, ForeignKey("site.id"), nullable=False, index=True
)
site: Mapped["Site"] = relationship("Site", back_populates="answers")
question_id: Mapped[int] = Column(
Integer, ForeignKey("question.id"), nullable=False, index=True
)
question: Mapped["Question"] = relationship("Question", back_populates="answers")

upvotes_count = Column(Integer, default=0, nullable=False)
upvotes_count: Mapped[int] = Column(Integer, default=0, nullable=False)

is_hidden_by_moderator = Column(Boolean, server_default="false", nullable=False)
is_hidden_by_moderator: Mapped[bool] = Column(
Boolean, server_default="false", nullable=False
)

# If not `is_published`, `body` is the author-only draft.
# Otherwise, `body` is the latest published text.
body = Column(String, nullable=False)
body: Mapped[str] = Column(String, nullable=False)

body_prerendered_text = Column(String, nullable=False)
keywords = Column(JSON)
body_prerendered_text: Mapped[str] = Column(String, nullable=False)
keywords: Mapped[Optional[Any]] = Column(JSON)

# Not null only if is_published is `True`, in which case it might contain a working draft version.
body_draft = Column(String)
body_draft: Mapped[Optional[str]] = Column(String)

editor: editor_T = Column(String, nullable=False, server_default="wysiwyg") # type: ignore
editor: Mapped[str] = Column(String, nullable=False, server_default="wysiwyg")

draft_saved_at = Column(DateTime(timezone=True))
draft_editor: editor_T = Column(String, nullable=False, server_default="wysiwyg") # type: ignore
draft_saved_at: Mapped[Optional[datetime.datetime]] = Column(DateTime(timezone=True))
draft_editor: Mapped[str] = Column(String, nullable=False, server_default="wysiwyg")

# Whether `body` contains the latest published version
is_published = Column(
is_published: Mapped[bool] = Column(
Boolean, default=False, server_default="false", nullable=False
)
# Whether `body` contains the latest published version
is_deleted = Column(Boolean, default=False, server_default="false", nullable=False)
is_deleted: Mapped[bool] = Column(
Boolean, default=False, server_default="false", nullable=False
)
# Time of the latest publication
updated_at = Column(DateTime(timezone=True), nullable=False)
updated_at: Mapped[datetime.datetime] = Column(DateTime(timezone=True), nullable=False)

archives: List["Archive"] = relationship("Archive", back_populates="answer", order_by="Archive.created_at.desc()") # type: ignore
archives: Mapped[List["Archive"]] = relationship(
"Archive", back_populates="answer", order_by="Archive.created_at.desc()"
)

comments: List["Comment"] = relationship("Comment", back_populates="answer", order_by="Comment.created_at.asc()") # type: ignore
comments: Mapped[List["Comment"]] = relationship(
"Comment", back_populates="answer", order_by="Comment.created_at.asc()"
)

# If in public site: World visible > registered user visible > [my friends visible -- in future]
# If in private site: site members visible > [my friends visible -- in future]
# https://stackoverflow.com/questions/37848815/sqlalchemy-postgresql-enum-does-not-create-type-on-db-migrate
visibility: ContentVisibility = Column(
visibility: Mapped[ContentVisibility] = Column(
Enum(ContentVisibility), nullable=False, server_default="ANYONE"
) # type: ignore
)

suggest_edits: List["AnswerSuggestEdit"] = relationship("AnswerSuggestEdit", back_populates="answer", order_by="AnswerSuggestEdit.created_at.desc()") # type: ignore
suggest_edits: Mapped[List["AnswerSuggestEdit"]] = relationship(
"AnswerSuggestEdit",
back_populates="answer",
order_by="AnswerSuggestEdit.created_at.desc()",
)

contributors: List["User"] = relationship( # type: ignore
contributors: Mapped[List["User"]] = relationship(
"User",
secondary=answer_contributors,
backref=backref(
Expand All @@ -105,6 +128,8 @@ class Answer(Base):
),
)

featured_at = Column(DateTime(timezone=True))
featured_at: Mapped[Optional[datetime.datetime]] = Column(DateTime(timezone=True))

reports: List["Report"] = relationship("Report", back_populates="answer", order_by="Report.created_at.asc()") # type: ignore
reports: Mapped[List["Report"]] = relationship(
"Report", back_populates="answer", order_by="Report.created_at.asc()"
)
91 changes: 55 additions & 36 deletions chafan_core/app/models/article.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import TYPE_CHECKING, List, Optional
import datetime
from typing import TYPE_CHECKING, Any, List, Optional

from sqlalchemy import (
CHAR,
Expand All @@ -12,7 +13,7 @@
Table,
UniqueConstraint,
)
from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm import Mapped, backref, relationship
from sqlalchemy.sql.sqltypes import JSON, Enum

from chafan_core.db.base_class import Base
Expand All @@ -37,26 +38,34 @@ class ArticleUpvotes(Base):
PrimaryKeyConstraint("article_id", "voter_id"),
)

cancelled = Column(Boolean, server_default="false", default=False, nullable=False)
article_id = Column(Integer, ForeignKey("article.id"), index=True)
article = relationship("Article", foreign_keys=[article_id])
voter_id = Column(Integer, ForeignKey("user.id"), index=True)
voter = relationship("User", foreign_keys=[voter_id])
cancelled: Mapped[bool] = Column(
Boolean, server_default="false", default=False, nullable=False
)
article_id: Mapped[int] = Column(Integer, ForeignKey("article.id"), index=True)
article: Mapped["Article"] = relationship("Article", foreign_keys=[article_id])
voter_id: Mapped[int] = Column(Integer, ForeignKey("user.id"), index=True)
voter: Mapped["User"] = relationship("User", foreign_keys=[voter_id])


class Article(Base):
id = Column(Integer, primary_key=True, index=True)
uuid = Column(CHAR(length=UUID_LENGTH), index=True, unique=True, nullable=False)
author_id = Column(Integer, ForeignKey("user.id"), nullable=False, index=True)
author = relationship("User", back_populates="articles", foreign_keys=[author_id])
article_column_id = Column(
id: Mapped[int] = Column(Integer, primary_key=True, index=True)
uuid: Mapped[str] = Column(
CHAR(length=UUID_LENGTH), index=True, unique=True, nullable=False
)
author_id: Mapped[int] = Column(
Integer, ForeignKey("user.id"), nullable=False, index=True
)
author: Mapped["User"] = relationship(
"User", back_populates="articles", foreign_keys=[author_id]
)
article_column_id: Mapped[int] = Column(
Integer, ForeignKey("articlecolumn.id"), nullable=False, index=True
)
article_column: "ArticleColumn" = relationship(
article_column: Mapped["ArticleColumn"] = relationship(
"ArticleColumn", back_populates="articles", foreign_keys=[article_column_id]
) # type: ignore
)

topics: List["Topic"] = relationship( # type: ignore
topics: Mapped[List["Topic"]] = relationship(
"Topic",
secondary=article_topics,
backref=backref(
Expand All @@ -65,46 +74,56 @@ class Article(Base):
)

# content fields
title = Column(String, nullable=False)
title_draft: Optional[StrippedNonEmptyStr] = Column(String) # type: ignore
title: Mapped[str] = Column(String, nullable=False)
title_draft: Mapped[Optional[str]] = Column(String)

body = Column(String, nullable=False)
body_text = Column(String)
body: Mapped[str] = Column(String, nullable=False)
body_text: Mapped[Optional[str]] = Column(String)

# Not null only if is_published is `True`, in which case it might contain a working draft version.
body_draft = Column(String)
body_draft: Mapped[Optional[str]] = Column(String)

editor: editor_T = Column(String, nullable=False, server_default="wysiwyg") # type: ignore
editor: Mapped[str] = Column(String, nullable=False, server_default="wysiwyg")

created_at = Column(DateTime(timezone=True), nullable=False)
initial_published_at = Column(DateTime(timezone=True))
updated_at = Column(DateTime(timezone=True)) # published_at
draft_saved_at = Column(DateTime(timezone=True))
draft_editor: editor_T = Column(String, nullable=False, server_default="wysiwyg") # type: ignore
created_at: Mapped[datetime.datetime] = Column(DateTime(timezone=True), nullable=False)
initial_published_at: Mapped[Optional[datetime.datetime]] = Column(
DateTime(timezone=True)
)
updated_at: Mapped[Optional[datetime.datetime]] = Column(
DateTime(timezone=True)
) # published_at
draft_saved_at: Mapped[Optional[datetime.datetime]] = Column(DateTime(timezone=True))
draft_editor: Mapped[str] = Column(String, nullable=False, server_default="wysiwyg")

is_published = Column(
is_published: Mapped[bool] = Column(
Boolean, default=False, server_default="false", nullable=False
)
is_deleted: Mapped[bool] = Column(
Boolean, default=False, server_default="false", nullable=False
)
is_deleted = Column(Boolean, default=False, server_default="false", nullable=False)

comments: List["Comment"] = relationship( # type: ignore
comments: Mapped[List["Comment"]] = relationship(
"Comment", back_populates="article", order_by="Comment.created_at.asc()"
)

upvotes_count = Column(Integer, default=0, server_default="0", nullable=False)
upvotes_count: Mapped[int] = Column(
Integer, default=0, server_default="0", nullable=False
)

archives: List["ArticleArchive"] = relationship(
archives: Mapped[List["ArticleArchive"]] = relationship(
"ArticleArchive",
back_populates="article",
order_by="ArticleArchive.created_at.desc()",
) # type: ignore
)

visibility = Column(
visibility: Mapped[ContentVisibility] = Column(
Enum(ContentVisibility), nullable=False, server_default="ANYONE"
)

keywords: List[str] = Column(JSON) # type: ignore
keywords: Mapped[Optional[Any]] = Column(JSON)

featured_at = Column(DateTime(timezone=True))
featured_at: Mapped[Optional[datetime.datetime]] = Column(DateTime(timezone=True))

reports: List["Report"] = relationship("Report", back_populates="article", order_by="Report.created_at.asc()") # type: ignore
reports: Mapped[List["Report"]] = relationship(
"Report", back_populates="article", order_by="Report.created_at.asc()"
)
Loading
Loading