diff --git a/app/__init__.py b/app/__init__.py
index ebb9641df3e9042f0831eb4dc4dbf85967de37d2..84a1ca2d23f1cd72ccdd33676845594753371613 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -6,8 +6,22 @@
# Author: GaoNingNing
# Date: 2022/11/27
# -------------------------------------------------------------------------------
-from flask import Flask
+from flask import Flask, render_template
+from flask_bootstrap import Bootstrap
+from flask_mail import Mail
+from flask_moment import Moment
+from flask_sqlalchemy import SQLAlchemy
from config import config
+from flask_login import LoginManager
+from flask_pagedown import PageDown
+
+bootstrap = Bootstrap()
+pagedown = PageDown()
+mail = Mail()
+moment = Moment()
+db = SQLAlchemy()
+login_manager = LoginManager()
+login_manager.login_view = 'auth.login'
def create_app(config_name):
@@ -15,7 +29,18 @@ def create_app(config_name):
app.config.from_object(config[config_name])
config[config_name].init_app(app)
- # 添加主路由
+ bootstrap.init_app(app)
+ mail.init_app(app)
+ moment.init_app(app)
+ db.init_app(app)
+ login_manager.init_app(app)
+ pagedown.init_app(app)
+
+ # 添加路由和自定义的错误页面
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
+
+ from .auth import auth as auth_blueprint
+ app.register_blueprint(auth_blueprint, url_prefix='/auth')
+
return app
diff --git a/app/auth/__init__.py b/app/auth/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..acc068b7c8cd4fe60e0bec5988c78d464a9e5c9e
--- /dev/null
+++ b/app/auth/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: __init__.py
+# Description: 身份验证模块
+# Author: GaoNingNing
+# Date: 2022/11/27
+# -------------------------------------------------------------------------------
+from flask import Blueprint
+
+auth = Blueprint('auth', __name__)
+
+from . import views
\ No newline at end of file
diff --git a/app/auth/forms.py b/app/auth/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..02d006fb1f4301f06feff13f7b8e84b68be6e838
--- /dev/null
+++ b/app/auth/forms.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: form
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/27
+# -------------------------------------------------------------------------------
+from flask_wtf import FlaskForm
+from wtforms import StringField, PasswordField, BooleanField, SubmitField
+from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
+from wtforms import ValidationError
+from ..models import User
+
+
+class RegistrationForm(FlaskForm):
+ username = StringField('Username', validators=[
+ DataRequired(), Length(1, 64),
+ Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
+ 'Usernames must have only letters, numbers, dots or '
+ 'underscores')])
+ password = PasswordField('Password', validators=[
+ DataRequired(), EqualTo('password2', message='Passwords must match.')])
+ password2 = PasswordField('Confirm password', validators=[DataRequired()])
+ submit = SubmitField('Register')
+
+ def validate_username(self, field):
+ if User.query.filter_by(username=field.data).first():
+ raise ValidationError('Username already in use.')
+
+
+class LoginForm(FlaskForm):
+ username = StringField('Username', validators=[DataRequired()])
+ password = PasswordField('Password', validators=[DataRequired()])
+ remember_me = BooleanField('Keep me logged in')
+ submit = SubmitField('Log In')
diff --git a/app/auth/views.py b/app/auth/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..f06f3d694207628e0a556644e88ecc12f68a4aff
--- /dev/null
+++ b/app/auth/views.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: views
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/27
+# -------------------------------------------------------------------------------
+from flask import render_template, redirect, request, url_for, flash
+from flask_login import login_user, current_user
+from . import auth
+from ..models import User
+from .forms import LoginForm, RegistrationForm
+from flask_login import logout_user, login_required
+from app import db
+
+
+@auth.route('/login', methods=['GET', 'POST'])
+def login():
+ form = LoginForm()
+ if form.validate_on_submit():
+ user = User.query.filter_by(username=form.username.data).first()
+ if user is not None and user.verify_password(form.password.data):
+ login_user(user, form.remember_me.data)
+ next = request.args.get('next')
+ if next is None or not next.startswith('/'):
+ next = url_for('main.index')
+ return redirect(next)
+ flash('Invalid username or password.')
+ return render_template('auth/login.html', form=form)
+
+
+@auth.route('/logout')
+@login_required
+def logout():
+ logout_user()
+ flash('You have been logged out.')
+ return redirect(url_for('main.index'))
+
+
+@auth.route('/register', methods=['GET', 'POST'])
+def register():
+ form = RegistrationForm()
+ if form.validate_on_submit():
+ user = User(username=form.username.data,
+ password=form.password.data)
+ db.session.add(user)
+ db.session.commit()
+ flash('You can now login.')
+ return redirect(url_for('auth.login'))
+ return render_template('auth/register.html', form=form)
+
+
+@auth.before_app_request
+def before_request():
+ if current_user.is_authenticated:
+ current_user.ping()
+ # if not current_user.confirmed \
+ # and request.endpoint \
+ # and request.blueprint != 'auth' \
+ # and request.endpoint != 'static':
+ # return redirect(url_for('auth.unconfirmed'))
diff --git a/app/decorators.py b/app/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4e3826aba9cc245bdf03bc9f938736fec502288
--- /dev/null
+++ b/app/decorators.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: decorators
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/27
+# -------------------------------------------------------------------------------
+from functools import wraps
+from flask import abort
+from flask_login import current_user
+from .models import Permission
+
+def permission_required(permission):
+ def decorator(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if not current_user.can(permission):
+ abort(403)
+ return f(*args, **kwargs)
+ return decorated_function
+ return decorator
+
+def admin_required(f):
+ return permission_required(Permission.ADMIN)(f)
diff --git a/app/main/__init__.py b/app/main/__init__.py
index fa38bdd87eb224f3a9d61f5c10446aa999c8eecb..4829f545cb523773619c9a23ec4339d9fa08cb18 100644
--- a/app/main/__init__.py
+++ b/app/main/__init__.py
@@ -9,6 +9,17 @@
from flask import Blueprint
# 主蓝本
+from ..models import Permission
+
main = Blueprint('main', __name__)
from . import views
+
+
+@main.app_context_processor
+def inject_permissions():
+ """
+ 使用上下文处理器。在渲染时,上下文处理器能让变量在所有模板中可访问, 为了避免每次调用render_template()时都多添加一个模板参数
+ :return:
+ """
+ return dict(Permission=Permission)
diff --git a/app/main/errors.py b/app/main/errors.py
new file mode 100644
index 0000000000000000000000000000000000000000..176544fd33a29b476dd13acdfaf2579dae81cea0
--- /dev/null
+++ b/app/main/errors.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: errors
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/27
+# -------------------------------------------------------------------------------
+from flask import render_template
+from . import main
+
+@main.app_errorhandler(404)
+def page_not_found(e):
+ return render_template('404.html'), 404
+
+@main.app_errorhandler(500)
+def internal_server_error(e):
+ return render_template('500.html'), 500
diff --git a/app/main/forms.py b/app/main/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..571edf71a900e6eea09a83069a46dddeb9adf762
--- /dev/null
+++ b/app/main/forms.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: form
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/27
+# -------------------------------------------------------------------------------
+from flask_pagedown.fields import PageDownField
+from flask_wtf import FlaskForm
+from wtforms import StringField, SubmitField, TextAreaField, BooleanField, SelectField
+from wtforms.validators import DataRequired, Length, Email, Regexp, ValidationError, input_required
+
+from app.models import Role, User
+
+
+class NameForm(FlaskForm):
+ name = StringField('What is your name?', validators=[DataRequired()])
+ submit = SubmitField('Submit')
+
+class EditProfileForm(FlaskForm):
+ name = StringField('Real name', validators=[Length(0, 64)])
+ location = StringField('Location', validators=[Length(0, 64)])
+ about_me = TextAreaField('About me')
+ submit = SubmitField('Submit')
+
+class EditProfileAdminForm(FlaskForm):
+ username = StringField('Username', validators=[
+ DataRequired(), Length(1, 64),
+ Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
+ 'Usernames must have only letters, numbers, dots or '
+ 'underscores')])
+ confirmed = BooleanField('Confirmed')
+ role = SelectField('Role', coerce=int)
+ name = StringField('Real name', validators=[Length(0, 64)])
+ location = StringField('Location', validators=[Length(0, 64)])
+ about_me = TextAreaField('About me')
+ submit = SubmitField('Submit')
+
+ def __init__(self, user, *args, **kwargs):
+ super(EditProfileAdminForm, self).__init__(*args, **kwargs)
+ self.role.choices = [(role.id, role.name)
+ for role in Role.query.order_by(Role.name).all()]
+ self.user = user
+
+ def validate_email(self, field):
+ if field.data != self.user.email and \
+ User.query.filter_by(email=field.data).first():
+ raise ValidationError('Email already registered.')
+
+ def validate_username(self, field):
+ if field.data != self.user.username and \
+ User.query.filter_by(username=field.data).first():
+ raise ValidationError('Username already in use.')
+
+class PostForm(FlaskForm):
+ body = PageDownField("What's on your mind?", validators=[input_required()])
+ submit = SubmitField('Submit')
+
+class CommentForm(FlaskForm):
+ body = StringField('', validators=[DataRequired()])
+ submit = SubmitField('Submit')
diff --git a/app/main/views.py b/app/main/views.py
index 3f36557d76ba251d7595aa9d019a8ce0e482a93c..f12b3de1685cc2b3a89b1c2c03a29973c9a0740d 100644
--- a/app/main/views.py
+++ b/app/main/views.py
@@ -6,9 +6,250 @@
# Author: GaoNingNing
# Date: 2022/11/27
# -------------------------------------------------------------------------------
-from app.main import main
+from flask import render_template, redirect, url_for, flash, abort, request, current_app, make_response
+from flask_login import login_required, current_user
+from . import main
+from .forms import EditProfileForm, EditProfileAdminForm, PostForm, CommentForm
+from .. import db
+from ..decorators import admin_required, permission_required
+from ..models import User, Role, Permission, Post, Comment
-@main.route('/')
+
+@main.route('/', methods=['GET', 'POST'])
def index():
- return '
Hello World!
'
+ form = PostForm()
+ print(Permission.WRITE)
+ if current_user.can(Permission.WRITE) and form.validate_on_submit():
+ post = Post(body=form.body.data,
+ author=current_user._get_current_object())
+ db.session.add(post)
+ db.session.commit()
+ return redirect(url_for('.index'))
+ show_followed = False
+ if current_user.is_authenticated:
+ show_followed = bool(request.cookies.get('show_followed', ''))
+ if show_followed:
+ query = current_user.followed_posts
+ else:
+ query = Post.query
+ pagination = query.order_by(Post.timestamp.desc()).paginate(
+ per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
+ error_out=False)
+ posts = pagination.items
+ return render_template('index.html', form=form, posts=posts,
+ show_followed=show_followed, pagination=pagination)
+
+
+@main.route('/all')
+@login_required
+def show_all():
+ resp = make_response(redirect(url_for('.index')))
+ resp.set_cookie('show_followed', '', max_age=30 * 24 * 60 * 60) # 30天
+ return resp
+
+
+@main.route('/followed')
+@login_required
+def show_followed():
+ resp = make_response(redirect(url_for('.index')))
+ resp.set_cookie('show_followed', '1', max_age=30 * 24 * 60 * 60) # 30天
+ return resp
+
+
+@main.route('/user/')
+def user(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ abort(404)
+ posts = user.posts.order_by(Post.timestamp.desc()).all()
+ return render_template('user.html', user=user, posts=posts)
+
+
+@main.route('/edit-profile', methods=['GET', 'POST'])
+@login_required
+def edit_profile():
+ form = EditProfileForm()
+ if form.validate_on_submit():
+ current_user.name = form.name.data
+ current_user.location = form.location.data
+ current_user.about_me = form.about_me.data
+ db.session.add(current_user._get_current_object())
+ db.session.commit()
+ flash('Your profile has been updated.')
+ return redirect(url_for('.user', username=current_user.username))
+ form.name.data = current_user.name
+ form.location.data = current_user.location
+ form.about_me.data = current_user.about_me
+ return render_template('edit_profile.html', form=form)
+
+
+@main.route('/edit-profile/', methods=['GET', 'POST'])
+@login_required
+@admin_required
+def edit_profile_admin(id):
+ user = User.query.get_or_404(id)
+ form = EditProfileAdminForm(user=user)
+ if form.validate_on_submit():
+ user.username = form.username.data
+ user.confirmed = form.confirmed.data
+ user.role = Role.query.get(form.role.data)
+ user.name = form.name.data
+ user.location = form.location.data
+ user.about_me = form.about_me.data
+ db.session.add(user)
+ db.session.commit()
+ flash('The profile has been updated.')
+ return redirect(url_for('.user', username=user.username))
+ form.username.data = user.username
+ form.confirmed.data = user.confirmed
+ form.role.data = user.role_id
+ form.name.data = user.name
+ form.location.data = user.location
+ form.about_me.data = user.about_me
+ return render_template('edit_profile.html', form=form, user=user)
+
+
+@main.route('/post/', methods=['GET', 'POST'])
+def post(id):
+ post = Post.query.get_or_404(id)
+ form = CommentForm()
+ if form.validate_on_submit():
+ comment = Comment(body=form.body.data,
+ post=post,
+ author=current_user._get_current_object())
+ db.session.add(comment)
+ db.session.commit()
+ flash('Your comment has been published.')
+ return redirect(url_for('.post', id=post.id, page=-1))
+ page = request.args.get('page', 1, type=int)
+ if page == -1:
+ page = (post.comments.count() - 1) // \
+ current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
+ pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
+ page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
+ error_out=False)
+ comments = pagination.items
+ return render_template('post.html', posts=[post], form=form,
+ comments=comments, pagination=pagination)
+
+
+@main.route('/edit/', methods=['GET', 'POST'])
+@login_required
+def edit(id):
+ post = Post.query.get_or_404(id)
+ if current_user != post.author and \
+ not current_user.can(Permission.ADMIN):
+ abort(403)
+ form = PostForm()
+ if form.validate_on_submit():
+ post.body = form.body.data
+ db.session.add(post)
+ db.session.commit()
+ flash('The post has been updated.')
+ return redirect(url_for('.post', id=post.id))
+ form.body.data = post.body
+ return render_template('edit_post.html', form=form)
+
+
+@main.route('/follow/')
+@login_required
+@permission_required(Permission.FOLLOW)
+def follow(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ flash('Invalid user.')
+ return redirect(url_for('.index'))
+ if current_user.is_following(user):
+ flash('You are already following this user.')
+ return redirect(url_for('.user', username=username))
+ current_user.follow(user)
+ db.session.commit()
+ flash('You are now following %s.' % username)
+ return redirect(url_for('.user', username=username))
+
+
+@main.route('/unfollow/')
+@login_required
+@permission_required(Permission.FOLLOW)
+def unfollow(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ flash('Invalid user.')
+ return redirect(url_for('.index'))
+ if not current_user.is_following(user):
+ flash('You are not following this user.')
+ return redirect(url_for('.user', username=username))
+ current_user.unfollow(user)
+ db.session.commit()
+ flash('You are now unfollow %s.' % username)
+ return redirect(url_for('.user', username=username))
+
+
+@main.route('/followers/')
+def followers(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ flash('Invalid user.')
+ return redirect(url_for('.index'))
+ page = request.args.get('page', 1, type=int)
+ pagination = user.followers.paginate(
+ per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
+ error_out=False)
+ follows = [{'user': item.follower, 'timestamp': item.timestamp}
+ for item in pagination.items]
+ return render_template('followers.html', user=user, title="Followers of",
+ endpoint='.followers', pagination=pagination,
+ follows=follows)
+
+
+@main.route('/followed_by/')
+def followed_by(username):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ flash('Invalid user.')
+ return redirect(url_for('.index'))
+ page = request.args.get('page', 1, type=int)
+ pagination = user.followed.paginate(
+ per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
+ error_out=False)
+ follows = [{'user': item.follower, 'timestamp': item.timestamp}
+ for item in pagination.items]
+ return render_template('followers.html', user=user, title="Followed By",
+ endpoint='.followers', pagination=pagination,
+ follows=follows)
+
+
+@main.route('/moderate')
+@login_required
+@permission_required(Permission.MODERATE)
+def moderate():
+ page = request.args.get('page', 1, type=int)
+ pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
+ page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
+ error_out=False)
+ comments = pagination.items
+ return render_template('moderate.html', comments=comments,
+ pagination=pagination, page=page)
+
+
+@main.route('/moderate/enable/')
+@login_required
+@permission_required(Permission.MODERATE)
+def moderate_enable(id):
+ comment = Comment.query.get_or_404(id)
+ comment.disabled = False
+ db.session.add(comment)
+ return redirect(url_for('.moderate',
+ page=request.args.get('page', 1, type=int)))
+
+
+@main.route('/moderate/disable/')
+@login_required
+@permission_required(Permission.MODERATE)
+def moderate_disable(id):
+ comment = Comment.query.get_or_404(id)
+ comment.disabled = True
+ db.session.add(comment)
+ return redirect(url_for('.moderate',
+ page=request.args.get('page', 1, type=int)))
diff --git a/app/models.py b/app/models.py
index d90c37e9965789f8e0ca273f6374042a4e0ee24e..9c45e288ae38e913018f6fc61467b3a6d59ef90e 100644
--- a/app/models.py
+++ b/app/models.py
@@ -6,3 +6,283 @@
# Author: GaoNingNing
# Date: 2022/11/27
# -------------------------------------------------------------------------------
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: models
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/20
+# -------------------------------------------------------------------------------
+import hashlib
+from datetime import datetime
+
+import bleach
+from flask import current_app, request, url_for
+from itsdangerous import Serializer
+from markdown import markdown
+from wtforms import ValidationError
+
+from app import db
+from werkzeug.security import generate_password_hash, check_password_hash
+from flask_login import UserMixin, AnonymousUserMixin
+from . import login_manager
+
+
+class Permission:
+ FOLLOW = 1
+ COMMENT = 2
+ WRITE = 4
+ MODERATE = 8
+ ADMIN = 16
+
+
+
+class Role(db.Model):
+ __tablename__ = 'roles'
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String(64), unique=True)
+ default = db.Column(db.Boolean, default=False, index=True)
+ permissions = db.Column(db.Integer)
+ users = db.relationship('User', backref='role', lazy='dynamic')
+
+ def __init__(self, **kwargs):
+ super(Role, self).__init__(**kwargs)
+ if self.permissions is None:
+ self.permissions = 0
+
+ def add_permission(self, perm):
+ if not self.has_permission(perm):
+ self.permissions += perm
+
+ def remove_permission(self, perm):
+ if self.has_permission(perm):
+ self.permissions -= perm
+
+ def reset_permissions(self):
+ self.permissions = 0
+
+ def has_permission(self, perm):
+ return self.permissions & perm == perm
+
+ @staticmethod
+ def insert_roles():
+ roles = {
+ 'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
+ 'Moderator': [Permission.FOLLOW, Permission.COMMENT,
+ Permission.WRITE, Permission.MODERATE],
+ 'Administrator': [Permission.FOLLOW, Permission.COMMENT,
+ Permission.WRITE, Permission.MODERATE,
+ Permission.ADMIN],
+ }
+ default_role = 'User'
+ for r in roles:
+ role = Role.query.filter_by(name=r).first()
+ if role is None:
+ role = Role(name=r)
+ role.reset_permissions()
+ for perm in roles[r]:
+ role.add_permission(perm)
+ role.default = (role.name == default_role)
+ db.session.add(role)
+ db.session.commit()
+
+class Post(db.Model):
+ __tablename__ = 'posts'
+ id = db.Column(db.Integer, primary_key=True)
+ body = db.Column(db.Text)
+ body_html = db.Column(db.Text)
+ timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
+ author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
+ comments = db.relationship('Comment', backref='post', lazy='dynamic')
+
+ @staticmethod
+ def on_changed_body(target, value, oldvalue, initiator):
+ allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
+ 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
+ 'h1', 'h2', 'h3', 'p']
+ target.body_html = bleach.linkify(bleach.clean(
+ markdown(value, output_format='html'),
+ tags=allowed_tags, strip=True))
+
+ def to_json(self):
+ json_post = {
+ 'url': url_for('api.get_post', id=self.id),
+ 'body': self.body,
+ 'body_html': self.body_html,
+ 'timestamp': self.timestamp,
+ 'author_url': url_for('api.get_user', id=self.author_id),
+ 'comments_url': url_for('api.get_post_comments', id=self.id),
+ 'comment_count': self.comments.count()
+ }
+ return json_post
+
+ @staticmethod
+ def from_json(json_post):
+ body = json_post.get('body')
+ if body is None or body == '':
+ raise ValidationError('post does not have a body')
+ return Post(body=body)
+
+class Follow(db.Model):
+ __tablename__ = 'follows'
+ follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
+ primary_key=True)
+ followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),
+ primary_key=True)
+ timestamp = db.Column(db.DateTime, default=datetime.utcnow)
+
+class User(UserMixin, db.Model):
+ __tablename__ = 'users'
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(64), unique=True, index=True)
+ password_hash = db.Column(db.String(128))
+ role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
+ name = db.Column(db.String(64))
+ location = db.Column(db.String(64))
+ about_me = db.Column(db.Text())
+ member_since = db.Column(db.DateTime(), default=datetime.utcnow)
+ last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
+ posts = db.relationship('Post', backref='author', lazy='dynamic')
+ followed = db.relationship('Follow',
+ foreign_keys=[Follow.follower_id],
+ backref=db.backref('follower', lazy='joined'),
+ lazy='dynamic',
+ cascade='all, delete-orphan')
+ followers = db.relationship('Follow',
+ foreign_keys=[Follow.followed_id],
+ backref=db.backref('followed', lazy='joined'),
+ lazy='dynamic',
+ cascade='all, delete-orphan')
+ comments = db.relationship('Comment', backref='author', lazy='dynamic')
+
+ def __init__(self, **kwargs):
+ super(User, self).__init__(**kwargs)
+ if self.role is None:
+ if self.role is None:
+ self.role = Role.query.filter_by(default=True).first()
+ self.follow(self)
+
+ @staticmethod
+ def add_self_follows():
+ for user in User.query.all():
+ if not user.is_following(user):
+ user.follow(user)
+ db.session.add(user)
+ db.session.commit()
+
+ @property
+ def password(self):
+ raise AttributeError('password is not a readable attribute')
+
+ @password.setter
+ def password(self, password):
+ self.password_hash = generate_password_hash(password)
+
+ def verify_password(self, password):
+ return check_password_hash(self.password_hash, password)
+
+ def __repr__(self):
+ return '' % self.username
+
+ def can(self, perm):
+ return self.role is not None and self.role.has_permission(perm)
+
+ def is_administrator(self):
+ return self.can(Permission.ADMIN)
+
+ def ping(self):
+ self.last_seen = datetime.utcnow()
+ db.session.add(self)
+ db.session.commit()
+
+ def follow(self, user):
+ if not self.is_following(user):
+ f = Follow(follower=self, followed=user)
+ db.session.add(f)
+
+ def unfollow(self, user):
+ f = self.followed.filter_by(followed_id=user.id).first()
+ if f:
+ db.session.delete(f)
+
+ def is_following(self, user):
+ if user.id is None:
+ return False
+ return self.followed.filter_by(
+ followed_id=user.id).first() is not None
+
+ def is_followed_by(self, user):
+ if user.id is None:
+ return False
+ return self.followers.filter_by(
+ follower_id=user.id).first() is not None
+
+ @property
+ def followed_posts(self):
+ return Post.query.join(Follow, Follow.followed_id == Post.author_id) \
+ .filter(Follow.follower_id == self.id)
+
+ def generate_auth_token(self, expiration):
+ s = Serializer(current_app.config['SECRET_KEY'])
+ return s.dumps({'id': self.id}).decode('utf-8')
+
+ @staticmethod
+ def verify_auth_token(token):
+ s = Serializer(current_app.config['SECRET_KEY'])
+ try:
+ data = s.loads(token)
+ except:
+ return None
+ return User.query.get(data['id'])
+
+ def to_json(self):
+ json_user = {
+ 'url': url_for('api.get_user', id=self.id),
+ 'username': self.username,
+ 'member_since': self.member_since,
+ 'last_seen': self.last_seen,
+ 'posts_url': url_for('api.get_user_posts', id=self.id),
+ 'followed_posts_url': url_for('api.get_user_followed_posts',
+ id=self.id),
+ 'post_count': self.posts.count()
+ }
+ return json_user
+
+
+class AnonymousUser(AnonymousUserMixin):
+ def can(self, permissions):
+ return False
+
+ def is_administrator(self):
+ return False
+
+
+login_manager.anonymous_user = AnonymousUser
+
+class Comment(db.Model):
+ __tablename__ = 'comments'
+ id = db.Column(db.Integer, primary_key=True)
+ body = db.Column(db.Text)
+ body_html = db.Column(db.Text)
+ timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
+ disabled = db.Column(db.Boolean)
+ author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
+ post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
+
+ @staticmethod
+ def on_changed_body(target, value, oldvalue, initiator):
+ allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',
+ 'strong']
+ target.body_html = bleach.linkify(bleach.clean(
+ markdown(value, output_format='html'),
+ tags=allowed_tags, strip=True))
+
+db.event.listen(Comment.body, 'set', Comment.on_changed_body)
+
+
+@login_manager.user_loader
+def load_user(user_id):
+ return User.query.get(int(user_id))
+
+db.event.listen(Post.body, 'set', Post.on_changed_body)
diff --git a/app/templates/404.html b/app/templates/404.html
new file mode 100644
index 0000000000000000000000000000000000000000..8194450792c7803ed9fcf2751503d5c90b1c2e31
--- /dev/null
+++ b/app/templates/404.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Flasky - Page Not Found{% endblock %}
+
+{% block page_content %}
+
+{% endblock %}
diff --git a/app/templates/500.html b/app/templates/500.html
new file mode 100644
index 0000000000000000000000000000000000000000..c6d21346d7aa1b99b3fef11361d0386c94e02a2c
--- /dev/null
+++ b/app/templates/500.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Flasky - Server Error{% endblock %}
+
+{% block page_content %}
+
+{% endblock %}
diff --git a/app/templates/__init__.py b/app/templates/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..709fee5c0232ed9a462f5495c72a864bc3b8bf13
--- /dev/null
+++ b/app/templates/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: __init__.py
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/19
+# -------------------------------------------------------------------------------
diff --git a/app/templates/_comments.html b/app/templates/_comments.html
new file mode 100644
index 0000000000000000000000000000000000000000..868110df793f1d3bc95a3a6ce0ff460b31628314
--- /dev/null
+++ b/app/templates/_comments.html
@@ -0,0 +1,40 @@
+
\ No newline at end of file
diff --git a/app/templates/_macros.html b/app/templates/_macros.html
new file mode 100644
index 0000000000000000000000000000000000000000..77ab54d06d8af7bb51244c7f341496abf2a67077
--- /dev/null
+++ b/app/templates/_macros.html
@@ -0,0 +1,31 @@
+{% macro pagination_widget(pagination, endpoint) %}
+
+{% endmacro %}
diff --git a/app/templates/_moderate.html b/app/templates/_moderate.html
new file mode 100644
index 0000000000000000000000000000000000000000..3f04f9ac79cc5793abe9d19a777788eaa805d8a6
--- /dev/null
+++ b/app/templates/_moderate.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+{% import "_macros.html" as macros %}
+
+{% block title %}Flasky - Comment Moderation{% endblock %}
+
+{% block page_content %}
+
+{% set moderate = True %}
+{% include '_comments.html' %}
+{% if pagination %}
+
+{% endif %}
+{% endblock %}
diff --git a/app/templates/_posts.html b/app/templates/_posts.html
new file mode 100644
index 0000000000000000000000000000000000000000..dbcae96557fbce1e865eafd71a3a8c842f25c91b
--- /dev/null
+++ b/app/templates/_posts.html
@@ -0,0 +1,40 @@
+
+ {% for post in posts %}
+ -
+
+
{{ moment(post.timestamp).fromNow() }}
+
+
+ {% if post.body_html %}
+ {{ post.body_html | safe }}
+ {% else %}
+ {{ post.body }}
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+
\ No newline at end of file
diff --git a/app/templates/auth/__init__.py b/app/templates/auth/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3025c3c01ea7f091a0f02c6e88acea3fd237788
--- /dev/null
+++ b/app/templates/auth/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-#
+
+# -------------------------------------------------------------------------------
+# Name: __init__.py
+# Description:
+# Author: GaoNingNing
+# Date: 2022/11/21
+# -------------------------------------------------------------------------------
diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..6bffeb72174dc507944d11fb52d3e523d3d7d397
--- /dev/null
+++ b/app/templates/auth/login.html
@@ -0,0 +1,20 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}Flasky - Login{% endblock %}
+
+{% block page_content %}
+
+
+ {{ wtf.quick_form(form) }}
+
+
+ New user?
+
+ Click here to register
+
+
+
+{% endblock %}
diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html
new file mode 100644
index 0000000000000000000000000000000000000000..349f47384984ed6ff7ea5a39305f663fd0e15b34
--- /dev/null
+++ b/app/templates/auth/register.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}Flasky - register{% endblock %}
+
+{% block page_content %}
+
+
+ {{ wtf.quick_form(form) }}
+
+
+{% endblock %}
diff --git a/app/templates/base.html b/app/templates/base.html
new file mode 100644
index 0000000000000000000000000000000000000000..b9cf02e5ff3c234efe24b5ee1c25d08180067109
--- /dev/null
+++ b/app/templates/base.html
@@ -0,0 +1,61 @@
+{% extends "bootstrap/base.html" %}
+
+{% block title %}Flasky{% endblock %}
+
+{% block navbar %}
+
+
+
+
+
+ - Home
+ {% if current_user.is_authenticated %}
+ -
+
+ Profile
+
+
+ {% endif %}
+
+
+
+
+ {% if current_user.is_authenticated %}
+ - Log Out
+ {% else %}
+ - Log In
+ {% endif %}
+
+
+
+
+{% endblock %}
+
+{% block content %}
+
+ {% for message in get_flashed_messages() %}
+
+
+ {{ message }}
+
+ {% endfor %}
+
+ {% block page_content %}{% endblock %}
+
+{% endblock %}
+
+
+{% block scripts %}
+{{ super() }}
+{{ moment.include_moment() }}
+{% endblock %}
+
diff --git a/app/templates/edit_post.html b/app/templates/edit_post.html
new file mode 100644
index 0000000000000000000000000000000000000000..7cec8f6ebf83abb5cdfdb3bdac65cf456fdae220
--- /dev/null
+++ b/app/templates/edit_post.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}Flasky - Edit Post{% endblock %}
+
+{% block page_content %}
+
+
+ {{ wtf.quick_form(form) }}
+
+{% endblock %}
+
+{% block scripts %}
+{{ super() }}
+{{ pagedown.include_pagedown() }}
+{% endblock %}
diff --git a/app/templates/edit_profile.html b/app/templates/edit_profile.html
new file mode 100644
index 0000000000000000000000000000000000000000..656af4651f1b322987ae14f572091b680d2d0e0c
--- /dev/null
+++ b/app/templates/edit_profile.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}Flasky - EditProfile{% endblock %}
+
+{% block page_content %}
+
+
+ {{ wtf.quick_form(form) }}
+
+{% endblock %}
diff --git a/app/templates/index.html b/app/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..70a808aafa45f2b8901d5c7cec5f19a74f6d0d4d
--- /dev/null
+++ b/app/templates/index.html
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+{% import "_macros.html" as macros %}
+
+{% block title %}Flasky{% endblock %}
+
+{% block page_content %}
+
+
+ {% if current_user.can(Permission.WRITE) %}
+ {{ wtf.quick_form(form) }}
+ {% endif %}
+
+
+{% include '_posts.html' %}
+
+{% endblock %}
+
+{% block scripts %}
+{{ super() }}
+{{ pagedown.include_pagedown() }}
+{% endblock %}
diff --git a/app/templates/post.html b/app/templates/post.html
new file mode 100644
index 0000000000000000000000000000000000000000..d5d11f6e4ec8a6a26091f819a9f7e183efed10ae
--- /dev/null
+++ b/app/templates/post.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block title %}Flasky - Post{% endblock %}
+
+{% block page_content %}
+{% include '_posts.html' %}
+{% endblock %}
diff --git a/app/templates/user.html b/app/templates/user.html
new file mode 100644
index 0000000000000000000000000000000000000000..56b858747edb9ef5c52e04b4b194ee40e80425d8
--- /dev/null
+++ b/app/templates/user.html
@@ -0,0 +1,64 @@
+{% extends "base.html" %}
+{% block title %}Flasky - {{ user.username }}{% endblock %}
+
+{% block page_content %}
+
+{% if current_user.can(Permission.FOLLOW) and user != current_user %}
+ {% if not current_user.is_following(user) %}
+ Follow
+ {% else %}
+ Unfollow
+ {% endif %}
+{% endif %}
+
+ Followers: {{ user.followers.count() - 1 }}
+
+
+ Following: {{ user.followed.count() - 1 }}
+
+{% if current_user.is_authenticated and user != current_user and
+ user.is_following(current_user) %}
+| Follows you
+{% endif %}
+
+
+Posts by {{ user.username }}
+{% include '_posts.html' %}
+{% endblock %}
diff --git a/config.py b/config.py
index 561a9257533d45a5ff887f6ee1e95e166e7c4f60..73b04d6fdde16dfb2086c7a2d236560188d56c38 100644
--- a/config.py
+++ b/config.py
@@ -15,7 +15,20 @@ class Config:
"""
配置基类
"""
- pass
+ SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
+ MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
+ MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
+ MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
+ ['true', 'on', '1']
+ MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
+ MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
+ FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
+ FLASKY_MAIL_SENDER = 'Flasky Admin '
+ FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
+ SQLALCHEMY_TRACK_MODIFICATIONS = False
+ FLASKY_POSTS_PER_PAGE = 5
+ FLASKY_FOLLOWERS_PER_PAGE = 5
+ FLASKY_COMMENTS_PER_PAGE = 5
@staticmethod
def init_app(app):
@@ -27,6 +40,8 @@ class DevelopmentConfig(Config):
开发环境配置
"""
DEBUG = True
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
+ 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config):
@@ -34,13 +49,16 @@ class TestingConfig(Config):
测试环境配置
"""
TESTING = True
+ SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
+ 'sqlite://'
class ProductionConfig(Config):
"""
生产环境配置
"""
- pass
+ SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
+ 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config = {
diff --git a/first_blog.py b/first_blog.py
index 4bb8fbdf5e93aef0662457b5772836e56678d05a..7ea0c7012d6a7b2ab9ba4c50c4b63de59ba41a0a 100644
--- a/first_blog.py
+++ b/first_blog.py
@@ -7,7 +7,17 @@
# Date: 2022/11/27
# -------------------------------------------------------------------------------
import os
-
-from app import create_app
+import sys
+import click
+from app import create_app, db
+from app.models import User, Role
+from flask_migrate import Migrate
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
+migrate = Migrate(app, db)
+
+
+@app.shell_context_processor
+def make_shell_context():
+ return dict(db=db, User=User, Role=Role)
+