diff --git a/greybook/core/extensions.py b/greybook/core/extensions.py index fcaf997..660d8e1 100644 --- a/greybook/core/extensions.py +++ b/greybook/core/extensions.py @@ -6,9 +6,24 @@ from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from flask_wtf import CSRFProtect +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + metadata = MetaData( + naming_convention={ + 'ix': 'ix_%(column_0_label)s', + 'uq': 'uq_%(table_name)s_%(column_0_name)s', + 'ck': 'ck_%(table_name)s_%(constraint_name)s', + 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s', + 'pk': 'pk_%(table_name)s', + } + ) + bootstrap = Bootstrap5() -db = SQLAlchemy() +db = SQLAlchemy(model_class=Base) login_manager = LoginManager() csrf = CSRFProtect() ckeditor = CKEditor() diff --git a/greybook/fakes.py b/greybook/fakes.py index 32e3488..e28c74d 100644 --- a/greybook/fakes.py +++ b/greybook/fakes.py @@ -18,8 +18,7 @@ def fake_admin(): blog_title='Greybook', blog_sub_title='Just some random thoughts', name='Grey Li', - about='Hello, I am Grey Li. This is an example ' - 'Flask project for my book.', + about='This is an example Flask project for this book.', ) db.session.add(admin) db.session.commit() @@ -42,7 +41,7 @@ def fake_categories(count=10): def fake_posts(count=50): for _ in range(count): - category_count = db.session.execute(select(func.count(Category.id))).scalars().one() + category_count = db.session.scalars(select(func.count(Category.id))).one() created_date = fake.date_time_between_dates( datetime_start=datetime(2010, 1, 1), datetime_end=datetime(2020, 1, 1) ) @@ -62,7 +61,7 @@ def fake_posts(count=50): def fake_comments(count=500): for _ in range(count): - post_count = db.session.execute(select(func.count(Post.id))).scalars().one() + post_count = db.session.scalars(select(func.count(Post.id))).one() comment = Comment( author=fake.name(), email=fake.email(), @@ -84,7 +83,7 @@ def fake_comments(count=500): def fake_replies(count=50): for _ in range(count): - comment_count = db.session.execute(select(func.count(Comment.id))).scalars().one() + comment_count = db.session.scalars(select(func.count(Comment.id))).one() replied = db.session.get(Comment, random.randint(1, comment_count)) comment = Comment( author=fake.name(), diff --git a/greybook/models.py b/greybook/models.py index a995966..d94216b 100644 --- a/greybook/models.py +++ b/greybook/models.py @@ -1,27 +1,33 @@ import os import re -from datetime import datetime +from datetime import datetime, timezone +from typing import List, Optional from flask import current_app, url_for from flask_login import UserMixin -from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text -from sqlalchemy.orm import relationship +from sqlalchemy import ForeignKey, String, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship from werkzeug.security import check_password_hash, generate_password_hash from greybook.core.extensions import db class Admin(db.Model, UserMixin): - id = Column(Integer, primary_key=True) - username = Column(String(20)) - password_hash = Column(String(128)) - blog_title = Column(String(60)) - blog_sub_title = Column(String(100)) - name = Column(String(30)) - about = Column(Text) - custom_footer = Column(Text) - custom_css = Column(Text) - custom_js = Column(Text) + __tablename__ = 'admin' + + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column(String(20)) + password_hash: Mapped[str] = mapped_column(String(128)) + blog_title: Mapped[str] = mapped_column(String(60)) + blog_sub_title: Mapped[str] = mapped_column(String(100)) + name: Mapped[str] = mapped_column(String(30)) + about: Mapped[str] = mapped_column(Text) + custom_footer: Mapped[Optional[str]] = mapped_column(Text) + custom_css: Mapped[Optional[str]] = mapped_column(Text) + custom_js: Mapped[Optional[str]] = mapped_column(Text) + + def __repr__(self): + return f'' @property def password(self): @@ -36,10 +42,15 @@ def validate_password(self, password): class Category(db.Model): - id = Column(Integer, primary_key=True) - name = Column(String(30), unique=True) + __tablename__ = 'category' - posts = relationship('Post', back_populates='category') + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(30), unique=True) + + posts: Mapped[List['Post']] = relationship(back_populates='category') + + def __repr__(self): + return f'' def delete(self): default_category = db.session.get(Category, 1) @@ -51,17 +62,22 @@ def delete(self): class Post(db.Model): - id = Column(Integer, primary_key=True) - title = Column(String(60)) - body = Column(Text) - created_at = Column(DateTime, default=datetime.utcnow, index=True) - updated_at = Column(DateTime, onupdate=datetime.utcnow) - can_comment = Column(Boolean, default=True) + __tablename__ = 'post' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(60)) + body: Mapped[str] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc), index=True) + updated_at: Mapped[Optional[datetime]] = mapped_column(onupdate=lambda: datetime.now(timezone.utc)) + can_comment: Mapped[bool] = mapped_column(default=True) - category_id = Column(Integer, ForeignKey('category.id')) + category_id: Mapped[int] = mapped_column(ForeignKey('category.id')) - category = relationship('Category', back_populates='posts') - comments = relationship('Comment', back_populates='post', cascade='all, delete-orphan') + category: Mapped['Category'] = relationship(back_populates='posts') + comments: Mapped[List['Comment']] = relationship(back_populates='post', cascade='all, delete-orphan') + + def __repr__(self): + return f'' @property def reviewed_comments_count(self): @@ -80,27 +96,34 @@ def delete(self): class Comment(db.Model): - id = Column(Integer, primary_key=True) - author = Column(String(30)) - email = Column(String(254)) - site = Column(String(255)) - body = Column(Text) - from_admin = Column(Boolean, default=False) - reviewed = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.utcnow, index=True) - - replied_id = Column(Integer, ForeignKey('comment.id')) - post_id = Column(Integer, ForeignKey('post.id')) - - post = relationship('Post', back_populates='comments') - replies = relationship('Comment', back_populates='replied', cascade='all, delete-orphan') - replied = relationship('Comment', back_populates='replies', remote_side=[id]) - # Same with: - # replies = relationship('Comment', backref=backref('replied', remote_side=[id]), - # cascade='all,delete-orphan') + __tablename__ = 'comment' + + id: Mapped[int] = mapped_column(primary_key=True) + author: Mapped[str] = mapped_column(String(30)) + email: Mapped[str] = mapped_column(String(255)) + site: Mapped[Optional[str]] = mapped_column(String(255)) + body: Mapped[str] = mapped_column(Text) + from_admin: Mapped[bool] = mapped_column(default=False) + reviewed: Mapped[bool] = mapped_column(default=False) + created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc), index=True) + + replied_id: Mapped[Optional[int]] = mapped_column(ForeignKey('comment.id')) + post_id: Mapped[int] = mapped_column(ForeignKey('post.id')) + + post: Mapped['Post'] = relationship(back_populates='comments') + replies: Mapped[List['Comment']] = relationship(back_populates='replied', cascade='all, delete-orphan') + replied: Mapped['Comment'] = relationship(back_populates='replies', remote_side=[id]) + + def __repr__(self): + return f'' class Link(db.Model): - id = Column(Integer, primary_key=True) - name = Column(String(30)) - url = Column(String(255)) + __tablename__ = 'link' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(30)) + url: Mapped[str] = mapped_column(String(255)) + + def __repr__(self): + return f'' diff --git a/greybook/settings.py b/greybook/settings.py index bc43599..f7d31c9 100644 --- a/greybook/settings.py +++ b/greybook/settings.py @@ -2,10 +2,8 @@ import sys from pathlib import Path -basedir = Path(__file__).resolve().parent.parent - -# SQLite URI compatible -prefix = 'sqlite:///' if sys.platform.startswith('win') else 'sqlite:////' +BASE_DIR = Path(__file__).resolve().parent.parent +SQLITE_PREFIX = 'sqlite:///' if sys.platform.startswith('win') else 'sqlite:////' class BaseConfig: @@ -26,7 +24,7 @@ class BaseConfig: MAIL_PASSWORD = os.getenv('MAIL_PASSWORD') MAIL_DEFAULT_SENDER = f'Greybook <{MAIL_USERNAME}>' - GREYBOOK_ADMIN_EMAIL = os.getenv('GREYBOOK_ADMIN_EMAIL') + GREYBOOK_ADMIN_EMAIL = os.getenv('GREYBOOK_ADMIN_EMAIL', 'admin@helloflask.com') GREYBOOK_POST_PER_PAGE = 10 GREYBOOK_MANAGE_POST_PER_PAGE = 15 GREYBOOK_COMMENT_PER_PAGE = 15 @@ -34,14 +32,14 @@ class BaseConfig: GREYBOOK_THEMES = {'default': 'Default', 'perfect_blue': 'Perfect Blue'} GREYBOOK_SLOW_QUERY_THRESHOLD = 1 - GREYBOOK_UPLOAD_PATH = os.getenv('GREYBOOK_UPLOAD_PATH', basedir / 'uploads') + GREYBOOK_UPLOAD_PATH = os.getenv('GREYBOOK_UPLOAD_PATH', BASE_DIR / 'uploads') GREYBOOK_ALLOWED_IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif'] - GREYBOOK_LOGGING_PATH = os.getenv('GREYBOOK_LOGGING_PATH', basedir / 'logs/greybook.log') + GREYBOOK_LOGGING_PATH = os.getenv('GREYBOOK_LOGGING_PATH', BASE_DIR / 'logs/greybook.log') GREYBOOK_ERROR_EMAIL_SUBJECT = '[Greybook] Application Error' class DevelopmentConfig(BaseConfig): - SQLALCHEMY_DATABASE_URI = prefix + str(basedir / 'data-dev.db') + SQLALCHEMY_DATABASE_URI = SQLITE_PREFIX + str(BASE_DIR / 'data-dev.db') class TestingConfig(BaseConfig): @@ -51,7 +49,7 @@ class TestingConfig(BaseConfig): class ProductionConfig(BaseConfig): - SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', prefix + str(basedir / 'data.db')) + SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', SQLITE_PREFIX + str(BASE_DIR / 'data.db')) config = {'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig} diff --git a/tests/__init__.py b/tests/__init__.py index 3762424..c5cde6e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -24,7 +24,9 @@ def setUp(self): ) category = Category(name='Test Category') post = Post(title='Test Post Title', category=category, body='Test post body') - comment = Comment(body='Test comment body', post=post, reviewed=True) + comment = Comment( + author='Test comment author', email='test@example.com', body='Test comment body', post=post, reviewed=True + ) link = Link(name='Test Link', url='http://example.com') db.session.add_all([user, category, post, comment, link]) db.session.commit() diff --git a/tests/test_admin.py b/tests/test_admin.py index 7c97404..a920f73 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -181,7 +181,7 @@ def test_new_category(self): self.assertIn('Name already in use.', data) category = db.session.get(Category, 1) - post = Post(title='Post Title', category=category) + post = Post(title='Post title', body='Post body', category=category) db.session.add(post) db.session.commit() response = self.client.get('/category/1') @@ -214,7 +214,7 @@ def test_edit_category(self): def test_delete_category(self): category = Category(name='Tech') - post = Post(title='test', category=category) + post = Post(title='Post title', body='Post body', category=category) db.session.add(category) db.session.add(post) db.session.commit()