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) %} +
    + + + « + + + {% for p in pagination.iter_pages() %} + {% if p %} + {% if p == pagination.page %} +
  • + {{ p }} +
  • + {% else %} +
  • + {{ p }} +
  • + {% endif %} + {% else %} +
  • + {% endif %} + {% endfor %} + + + » + + +
+{% 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 @@ + \ 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 %} + +{% 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) +