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()