feat: 初始版本,圆角主题与首次管理员引导
This commit is contained in:
37
app/blueprints/activities.py
Normal file
37
app/blueprints/activities.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from ..extensions import db
|
||||
from ..models import Activity, ActivityStatus, ActivitySubmission, SubmissionImage, ReviewStatus
|
||||
from ..services.images import save_image
|
||||
|
||||
bp = Blueprint("activities", __name__, url_prefix="/activities")
|
||||
|
||||
@bp.route("")
|
||||
def list_activities():
|
||||
acts = Activity.query.filter(Activity.status != ActivityStatus.draft).order_by(Activity.start_at).all()
|
||||
return render_template("activities/list.html", activities=acts)
|
||||
|
||||
@bp.route("/<int:act_id>")
|
||||
def detail(act_id):
|
||||
act = Activity.query.get_or_404(act_id)
|
||||
return render_template("activities/detail.html", activity=act)
|
||||
|
||||
@bp.route("/<int:act_id>/submit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def submit(act_id):
|
||||
act = Activity.query.get_or_404(act_id)
|
||||
if request.method == "POST":
|
||||
files = request.files.getlist("images")
|
||||
sub = ActivitySubmission(activity_id=act.id, user_id=current_user.id, status=ReviewStatus.pending)
|
||||
db.session.add(sub)
|
||||
db.session.flush()
|
||||
upload_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], "activities")
|
||||
for idx, f in enumerate(files):
|
||||
original, web, thumb, exif = save_image(f, upload_dir)
|
||||
img = SubmissionImage(submission_id=sub.id, original_path=original, web_path=web, thumb_path=thumb, exif_json=exif, order_index=idx)
|
||||
db.session.add(img)
|
||||
db.session.commit()
|
||||
flash("投稿已提交,待审核")
|
||||
return redirect(url_for("activities.detail", act_id=act.id))
|
||||
return render_template("activities/submit.html", activity=act)
|
||||
137
app/blueprints/admin.py
Normal file
137
app/blueprints/admin.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, flash
|
||||
from flask_login import login_required, current_user
|
||||
from ..extensions import db
|
||||
from ..models import User, UserStatus, Post, ReviewStatus, Activity, ActivityStatus, ActivitySubmission, ReviewLog
|
||||
from ..services.notify import notify
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
|
||||
def is_admin():
|
||||
return current_user.is_authenticated and current_user.role == "admin"
|
||||
|
||||
@bp.before_request
|
||||
def guard():
|
||||
if request.endpoint and request.endpoint.startswith("admin."):
|
||||
if not is_admin():
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
@bp.route("/")
|
||||
@login_required
|
||||
def dashboard():
|
||||
pending_users = User.query.filter_by(status=UserStatus.pending).count()
|
||||
pending_posts = Post.query.filter_by(status=ReviewStatus.pending).count()
|
||||
pending_subs = ActivitySubmission.query.filter_by(status=ReviewStatus.pending).count()
|
||||
return render_template("admin/dashboard.html", pending_users=pending_users, pending_posts=pending_posts, pending_subs=pending_subs)
|
||||
|
||||
@bp.route("/reviews/users")
|
||||
@login_required
|
||||
def review_users():
|
||||
users = User.query.filter_by(status=UserStatus.pending).all()
|
||||
return render_template("admin/reviews_users.html", users=users)
|
||||
|
||||
@bp.route("/reviews/users/<int:user_id>/approve", methods=["POST"])
|
||||
@login_required
|
||||
def approve_user(user_id):
|
||||
u = User.query.get_or_404(user_id)
|
||||
u.status = UserStatus.approved
|
||||
db.session.add(ReviewLog(target_type="user", target_id=u.id, admin_id=current_user.id, action="approve"))
|
||||
notify(u.id, "user_approved")
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.review_users"))
|
||||
|
||||
@bp.route("/reviews/users/<int:user_id>/reject", methods=["POST"])
|
||||
@login_required
|
||||
def reject_user(user_id):
|
||||
u = User.query.get_or_404(user_id)
|
||||
u.status = UserStatus.rejected
|
||||
db.session.add(ReviewLog(target_type="user", target_id=u.id, admin_id=current_user.id, action="reject"))
|
||||
notify(u.id, "user_rejected")
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.review_users"))
|
||||
|
||||
@bp.route("/reviews/posts")
|
||||
@login_required
|
||||
def review_posts():
|
||||
posts = Post.query.filter_by(status=ReviewStatus.pending).all()
|
||||
return render_template("admin/reviews_posts.html", posts=posts)
|
||||
|
||||
@bp.route("/reviews/posts/<int:post_id>/approve", methods=["POST"])
|
||||
@login_required
|
||||
def approve_post(post_id):
|
||||
p = Post.query.get_or_404(post_id)
|
||||
p.status = ReviewStatus.approved
|
||||
p.published_at = datetime.utcnow()
|
||||
db.session.add(ReviewLog(target_type="post", target_id=p.id, admin_id=current_user.id, action="approve"))
|
||||
notify(p.user_id, "post_approved")
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.review_posts"))
|
||||
|
||||
@bp.route("/reviews/posts/<int:post_id>/reject", methods=["POST"])
|
||||
@login_required
|
||||
def reject_post(post_id):
|
||||
p = Post.query.get_or_404(post_id)
|
||||
p.status = ReviewStatus.rejected
|
||||
db.session.add(ReviewLog(target_type="post", target_id=p.id, admin_id=current_user.id, action="reject"))
|
||||
notify(p.user_id, "post_rejected")
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.review_posts"))
|
||||
|
||||
@bp.route("/activities", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def manage_activities():
|
||||
if request.method == "POST":
|
||||
title = request.form.get("title")
|
||||
theme = request.form.get("theme")
|
||||
description = request.form.get("description")
|
||||
a = Activity(title=title, theme=theme, description=description)
|
||||
db.session.add(a)
|
||||
db.session.commit()
|
||||
flash("活动已创建")
|
||||
return redirect(url_for("admin.manage_activities"))
|
||||
acts = Activity.query.order_by(Activity.created_at.desc()).all()
|
||||
return render_template("admin/activities.html", activities=acts)
|
||||
|
||||
@bp.route("/activities/<int:act_id>/publish", methods=["POST"])
|
||||
@login_required
|
||||
def publish_activity(act_id):
|
||||
a = Activity.query.get_or_404(act_id)
|
||||
a.status = ActivityStatus.published
|
||||
db.session.add(ReviewLog(target_type="activity", target_id=a.id, admin_id=current_user.id, action="publish"))
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.manage_activities"))
|
||||
|
||||
@bp.route("/activities/<int:act_id>/close", methods=["POST"])
|
||||
@login_required
|
||||
def close_activity(act_id):
|
||||
a = Activity.query.get_or_404(act_id)
|
||||
a.status = ActivityStatus.closed
|
||||
db.session.add(ReviewLog(target_type="activity", target_id=a.id, admin_id=current_user.id, action="close"))
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.manage_activities"))
|
||||
|
||||
@bp.route("/reviews/submissions")
|
||||
@login_required
|
||||
def review_submissions():
|
||||
subs = ActivitySubmission.query.filter_by(status=ReviewStatus.pending).all()
|
||||
return render_template("admin/reviews_submissions.html", submissions=subs)
|
||||
|
||||
@bp.route("/reviews/submissions/<int:sub_id>/approve", methods=["POST"])
|
||||
@login_required
|
||||
def approve_submission(sub_id):
|
||||
s = ActivitySubmission.query.get_or_404(sub_id)
|
||||
s.status = ReviewStatus.approved
|
||||
db.session.add(ReviewLog(target_type="submission", target_id=s.id, admin_id=current_user.id, action="approve"))
|
||||
notify(s.user_id, "submission_approved")
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.review_submissions"))
|
||||
|
||||
@bp.route("/reviews/submissions/<int:sub_id>/reject", methods=["POST"])
|
||||
@login_required
|
||||
def reject_submission(sub_id):
|
||||
s = ActivitySubmission.query.get_or_404(sub_id)
|
||||
s.status = ReviewStatus.rejected
|
||||
db.session.add(ReviewLog(target_type="submission", target_id=s.id, admin_id=current_user.id, action="reject"))
|
||||
notify(s.user_id, "submission_rejected")
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.review_submissions"))
|
||||
61
app/blueprints/auth.py
Normal file
61
app/blueprints/auth.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.utils import secure_filename
|
||||
from ..extensions import db, login_manager
|
||||
from ..models import User, Profile, UserStatus
|
||||
|
||||
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
@bp.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email")
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
photo = request.files.get("identity_photo")
|
||||
if not email or not username or not password or not photo:
|
||||
flash("请完整填写信息并上传身份照片")
|
||||
return redirect(url_for("auth.register"))
|
||||
if User.query.filter_by(email=email).first() or User.query.filter_by(username=username).first():
|
||||
flash("邮箱或用户名已存在")
|
||||
return redirect(url_for("auth.register"))
|
||||
filename = secure_filename(photo.filename)
|
||||
upload_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], "identity")
|
||||
path = os.path.join(upload_dir, filename)
|
||||
photo.save(path)
|
||||
user = User(email=email, username=username, password_hash=generate_password_hash(password), status=UserStatus.pending, identity_photo_path=path)
|
||||
db.session.add(user)
|
||||
db.session.flush()
|
||||
profile = Profile(user_id=user.id)
|
||||
db.session.add(profile)
|
||||
db.session.commit()
|
||||
flash("注册提交成功,请等待管理员审核")
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("auth/register.html")
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email")
|
||||
password = request.form.get("password")
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if not user or not check_password_hash(user.password_hash, password):
|
||||
flash("登录失败")
|
||||
return redirect(url_for("auth.login"))
|
||||
if user.status != UserStatus.approved:
|
||||
flash("账户未审核通过")
|
||||
return redirect(url_for("auth.login"))
|
||||
login_user(user)
|
||||
return redirect(url_for("feed.discover"))
|
||||
return render_template("auth/login.html")
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("auth.login"))
|
||||
17
app/blueprints/comments.py
Normal file
17
app/blueprints/comments.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from flask import Blueprint, request, redirect, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from ..extensions import db
|
||||
from ..models import Comment, Post
|
||||
|
||||
bp = Blueprint("comments", __name__, url_prefix="/comments")
|
||||
|
||||
@bp.route("/create/<int:post_id>", methods=["POST"])
|
||||
@login_required
|
||||
def create(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
body = request.form.get("body")
|
||||
if body:
|
||||
c = Comment(post_id=post.id, user_id=current_user.id, body=body)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
return redirect(url_for("posts.detail", post_id=post.id))
|
||||
23
app/blueprints/feed.py
Normal file
23
app/blueprints/feed.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import desc
|
||||
from ..models import Post, Visibility, ReviewStatus, Follow
|
||||
|
||||
bp = Blueprint("feed", __name__, url_prefix="/feed")
|
||||
|
||||
@bp.route("/")
|
||||
def home():
|
||||
return discover()
|
||||
|
||||
@bp.route("/discover")
|
||||
def discover():
|
||||
posts = Post.query.filter(Post.visibility == Visibility.public, Post.status == ReviewStatus.approved).order_by(desc(Post.published_at)).limit(100).all()
|
||||
return render_template("feed/discover.html", posts=posts)
|
||||
|
||||
@bp.route("/following")
|
||||
@login_required
|
||||
def following():
|
||||
ids = [f.followee_id for f in Follow.query.filter_by(follower_id=current_user.id).all()]
|
||||
q = Post.query.filter(Post.user_id.in_(ids)).filter((Post.visibility == Visibility.followers) | (Post.visibility == Visibility.public))
|
||||
posts = q.order_by(desc(Post.created_at)).limit(100).all()
|
||||
return render_template("feed/following.html", posts=posts)
|
||||
25
app/blueprints/follows.py
Normal file
25
app/blueprints/follows.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from flask import Blueprint, redirect, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from ..extensions import db
|
||||
from ..models import Follow, User
|
||||
|
||||
bp = Blueprint("follows", __name__, url_prefix="/follows")
|
||||
|
||||
@bp.route("/follow/<int:user_id>")
|
||||
@login_required
|
||||
def follow(user_id):
|
||||
target = User.query.get_or_404(user_id)
|
||||
if target.id != current_user.id:
|
||||
exist = Follow.query.filter_by(follower_id=current_user.id, followee_id=target.id).first()
|
||||
if not exist:
|
||||
db.session.add(Follow(follower_id=current_user.id, followee_id=target.id))
|
||||
db.session.commit()
|
||||
return redirect(url_for("users.profile", user_id=target.id))
|
||||
|
||||
@bp.route("/unfollow/<int:user_id>")
|
||||
@login_required
|
||||
def unfollow(user_id):
|
||||
target = User.query.get_or_404(user_id)
|
||||
Follow.query.filter_by(follower_id=current_user.id, followee_id=target.id).delete()
|
||||
db.session.commit()
|
||||
return redirect(url_for("users.profile", user_id=target.id))
|
||||
11
app/blueprints/main.py
Normal file
11
app/blueprints/main.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from flask import Blueprint, redirect, url_for, current_app, send_from_directory
|
||||
|
||||
bp = Blueprint("main", __name__)
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
return redirect(url_for("feed.discover"))
|
||||
|
||||
@bp.route("/uploads/<path:filename>")
|
||||
def uploads(filename):
|
||||
return send_from_directory(current_app.config["UPLOAD_FOLDER"], filename)
|
||||
53
app/blueprints/posts.py
Normal file
53
app/blueprints/posts.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
from ..extensions import db
|
||||
from ..models import Post, PostImage, Visibility, ReviewStatus, Follow
|
||||
from ..services.images import save_image
|
||||
|
||||
bp = Blueprint("posts", __name__, url_prefix="/posts")
|
||||
|
||||
@bp.route("/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create():
|
||||
if request.method == "POST":
|
||||
title = request.form.get("title")
|
||||
description = request.form.get("description")
|
||||
visibility = request.form.get("visibility", Visibility.private.value)
|
||||
files = request.files.getlist("images")
|
||||
if not title or not files:
|
||||
flash("请填写标题并上传图片")
|
||||
return redirect(url_for("posts.create"))
|
||||
post = Post(user_id=current_user.id, title=title, description=description, visibility=Visibility(visibility))
|
||||
if post.visibility == Visibility.public:
|
||||
post.status = ReviewStatus.pending
|
||||
else:
|
||||
post.status = ReviewStatus.approved
|
||||
post.published_at = datetime.utcnow()
|
||||
db.session.add(post)
|
||||
db.session.flush()
|
||||
upload_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], "posts")
|
||||
for idx, f in enumerate(files):
|
||||
original, web, thumb, exif = save_image(f, upload_dir)
|
||||
img = PostImage(post_id=post.id, original_path=original, web_path=web, thumb_path=thumb, exif_json=exif, order_index=idx)
|
||||
db.session.add(img)
|
||||
db.session.commit()
|
||||
flash("作品已提交")
|
||||
return redirect(url_for("users.profile", user_id=current_user.id))
|
||||
return render_template("posts/create.html")
|
||||
|
||||
@bp.route("/<int:post_id>")
|
||||
def detail(post_id):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
if post.visibility == Visibility.private and (not current_user.is_authenticated or current_user.id != post.user_id):
|
||||
return redirect(url_for("auth.login"))
|
||||
if post.visibility == Visibility.followers:
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for("auth.login"))
|
||||
if current_user.id != post.user_id:
|
||||
f = Follow.query.filter_by(follower_id=current_user.id, followee_id=post.user_id).first()
|
||||
if not f:
|
||||
return redirect(url_for("auth.login"))
|
||||
return render_template("posts/detail.html", post=post)
|
||||
39
app/blueprints/setup.py
Normal file
39
app/blueprints/setup.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_user
|
||||
from werkzeug.security import generate_password_hash
|
||||
from ..extensions import db
|
||||
from ..models import User, Profile, UserStatus
|
||||
|
||||
bp = Blueprint("setup", __name__, url_prefix="/setup")
|
||||
|
||||
@bp.route("/admin", methods=["GET", "POST"])
|
||||
def admin():
|
||||
exists = User.query.filter_by(role="admin").first()
|
||||
if exists:
|
||||
return redirect(url_for("feed.discover"))
|
||||
if request.method == "POST":
|
||||
email = request.form.get("email")
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
confirm = request.form.get("confirm")
|
||||
if not email or not username or not password:
|
||||
flash("请完整填写信息")
|
||||
return redirect(url_for("setup.admin"))
|
||||
if password != confirm:
|
||||
flash("两次密码不一致")
|
||||
return redirect(url_for("setup.admin"))
|
||||
if len(password) < 8:
|
||||
flash("密码至少8位,并包含数字与字母")
|
||||
return redirect(url_for("setup.admin"))
|
||||
if User.query.filter((User.email==email) | (User.username==username)).first():
|
||||
flash("邮箱或用户名已存在")
|
||||
return redirect(url_for("setup.admin"))
|
||||
u = User(email=email, username=username, password_hash=generate_password_hash(password), role="admin", status=UserStatus.approved)
|
||||
db.session.add(u)
|
||||
db.session.flush()
|
||||
db.session.add(Profile(user_id=u.id))
|
||||
db.session.commit()
|
||||
login_user(u)
|
||||
flash("管理员创建成功")
|
||||
return redirect(url_for("admin.dashboard"))
|
||||
return render_template("setup/admin.html")
|
||||
29
app/blueprints/users.py
Normal file
29
app/blueprints/users.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from ..extensions import db
|
||||
from ..models import User, Profile, Notification
|
||||
|
||||
bp = Blueprint("users", __name__, url_prefix="/users")
|
||||
|
||||
@bp.route("/<int:user_id>")
|
||||
def profile(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
return render_template("users/profile.html", user=user)
|
||||
|
||||
@bp.route("/me/edit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_profile():
|
||||
profile = Profile.query.filter_by(user_id=current_user.id).first()
|
||||
if request.method == "POST":
|
||||
profile.bio = request.form.get("bio")
|
||||
profile.grade = request.form.get("grade")
|
||||
profile.class_name = request.form.get("class_name")
|
||||
db.session.commit()
|
||||
return redirect(url_for("users.profile", user_id=current_user.id))
|
||||
return render_template("users/edit.html", profile=profile)
|
||||
|
||||
@bp.route("/me/notifications")
|
||||
@login_required
|
||||
def notifications():
|
||||
items = Notification.query.filter_by(user_id=current_user.id).order_by(Notification.created_at.desc()).limit(50).all()
|
||||
return render_template("users/notifications.html", items=items)
|
||||
Reference in New Issue
Block a user