mirror of
https://github.com/minetest/contentdb.git
synced 2024-12-23 06:22:24 +01:00
eb9466f346
Fixes #355 Fixes #538
288 lines
9.4 KiB
Python
288 lines
9.4 KiB
Python
# ContentDB
|
|
# Copyright (C) 2018-21 rubenwardy
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import math
|
|
from typing import Optional, Tuple, List
|
|
|
|
from flask import redirect, url_for, abort, render_template, request
|
|
from flask_babel import gettext
|
|
from flask_login import current_user, login_required
|
|
from sqlalchemy import func, text
|
|
|
|
from app.models import User, db, Package, PackageReview, PackageState, PackageType, UserRank, Collection
|
|
from app.utils import get_daterange_options
|
|
from app.tasks.forumtasks import check_forum_account
|
|
|
|
from . import bp
|
|
|
|
|
|
@bp.route("/users/", methods=["GET"])
|
|
def list_all():
|
|
users = db.session.query(User, func.count(Package.id)) \
|
|
.select_from(User).outerjoin(Package) \
|
|
.order_by(db.desc(User.rank), db.asc(User.display_name)) \
|
|
.group_by(User.id).all()
|
|
|
|
return render_template("users/list.html", users=users)
|
|
|
|
|
|
@bp.route("/user/forum/<username>/")
|
|
def by_forums_username(username):
|
|
user = User.query.filter_by(forums_username=username).first()
|
|
if user:
|
|
return redirect(url_for("users.profile", username=user.username))
|
|
|
|
return render_template("users/forums_no_such_user.html", username=username)
|
|
|
|
|
|
class Medal:
|
|
description: str
|
|
color: Optional[str]
|
|
icon: str
|
|
title: Optional[str]
|
|
progress: Optional[Tuple[int, int]]
|
|
|
|
def __init__(self, description: str, **kwargs):
|
|
self.description = description
|
|
self.color = kwargs.get("color", "white")
|
|
self.icon = kwargs.get("icon", None)
|
|
self.title = kwargs.get("title", None)
|
|
self.progress = kwargs.get("progress", None)
|
|
|
|
@classmethod
|
|
def make_unlocked(cls, color: str, icon: str, title: str, description: str):
|
|
return Medal(description=description, color=color, icon=icon, title=title)
|
|
|
|
@classmethod
|
|
def make_locked(cls, description: str, progress: Tuple[int, int]):
|
|
if progress[0] is None or progress[1] is None:
|
|
raise Exception("Invalid progress")
|
|
|
|
return Medal(description=description, progress=progress)
|
|
|
|
|
|
def place_to_color(place: int) -> str:
|
|
if place == 1:
|
|
return "gold"
|
|
elif place == 2:
|
|
return "#888"
|
|
elif place == 3:
|
|
return "#cd7f32"
|
|
else:
|
|
return "white"
|
|
|
|
|
|
def get_user_medals(user: User) -> Tuple[List[Medal], List[Medal]]:
|
|
unlocked = []
|
|
locked = []
|
|
|
|
#
|
|
# REVIEWS
|
|
#
|
|
|
|
users_by_reviews = db.session.query(User.username, func.sum(PackageReview.score).label("karma")) \
|
|
.select_from(User).join(PackageReview) \
|
|
.group_by(User.username).order_by(text("karma DESC")).all()
|
|
try:
|
|
review_boundary = users_by_reviews[math.floor(len(users_by_reviews) * 0.25)][1] + 1
|
|
except IndexError:
|
|
review_boundary = None
|
|
usernames_by_reviews = [username for username, _ in users_by_reviews]
|
|
|
|
review_idx = None
|
|
review_percent = None
|
|
review_karma = 0
|
|
try:
|
|
review_idx = usernames_by_reviews.index(user.username)
|
|
review_percent = round(100 * review_idx / len(users_by_reviews), 1)
|
|
review_karma = max(users_by_reviews[review_idx][1], 0)
|
|
except ValueError:
|
|
pass
|
|
|
|
if review_percent is not None and review_percent < 25:
|
|
if review_idx == 0:
|
|
title = gettext(u"Top reviewer")
|
|
description = gettext(
|
|
u"%(display_name)s has written the most helpful reviews on ContentDB.",
|
|
display_name=user.display_name)
|
|
elif review_idx <= 2:
|
|
if review_idx == 1:
|
|
title = gettext(u"2nd most helpful reviewer")
|
|
else:
|
|
title = gettext(u"3rd most helpful reviewer")
|
|
description = gettext(
|
|
u"This puts %(display_name)s in the top %(perc)s%%",
|
|
display_name=user.display_name, perc=review_percent)
|
|
else:
|
|
title = gettext(u"Top %(perc)s%% reviewer", perc=review_percent)
|
|
description = gettext(u"Only %(place)d users have written more helpful reviews.", place=review_idx)
|
|
|
|
unlocked.append(Medal.make_unlocked(
|
|
place_to_color(review_idx + 1), "fa-star-half-alt", title, description))
|
|
elif review_boundary is not None:
|
|
description = gettext(u"Consider writing more helpful reviews to get a medal.")
|
|
if review_idx:
|
|
description += " " + gettext(u"You are in place %(place)s.", place=review_idx + 1)
|
|
locked.append(Medal.make_locked(
|
|
description, (review_karma, review_boundary)))
|
|
|
|
#
|
|
# TOP PACKAGES
|
|
#
|
|
all_package_ranks = db.session.query(
|
|
Package.type,
|
|
Package.author_id,
|
|
func.rank().over(
|
|
order_by=db.desc(Package.score),
|
|
partition_by=Package.type) \
|
|
.label("rank")).order_by(db.asc(text("rank"))) \
|
|
.filter_by(state=PackageState.APPROVED).subquery()
|
|
|
|
user_package_ranks = db.session.query(all_package_ranks) \
|
|
.filter_by(author_id=user.id) \
|
|
.filter(text("rank <= 30")) \
|
|
.all()
|
|
|
|
user_package_ranks = next(
|
|
(x for x in user_package_ranks if x[0] == PackageType.MOD or x[2] <= 10),
|
|
None)
|
|
if user_package_ranks:
|
|
top_rank = user_package_ranks[2]
|
|
top_type = PackageType.coerce(user_package_ranks[0])
|
|
title = top_type.get_top_ordinal(top_rank)
|
|
if top_type == PackageType.MOD:
|
|
icon = "fa-box"
|
|
elif top_type == PackageType.GAME:
|
|
icon = "fa-gamepad"
|
|
else:
|
|
icon = "fa-paint-brush"
|
|
|
|
description = top_type.get_top_ordinal_description(user.display_name, top_rank)
|
|
unlocked.append(
|
|
Medal.make_unlocked(place_to_color(top_rank), icon, title, description))
|
|
|
|
#
|
|
# DOWNLOADS
|
|
#
|
|
total_downloads = db.session.query(func.sum(Package.downloads)) \
|
|
.select_from(User) \
|
|
.join(User.packages) \
|
|
.filter(User.id == user.id,
|
|
Package.state == PackageState.APPROVED).scalar()
|
|
if total_downloads is None:
|
|
pass
|
|
elif total_downloads < 50000:
|
|
description = gettext(u"Your packages have %(downloads)d downloads in total.", downloads=total_downloads)
|
|
description += " " + gettext(u"First medal is at 50k.")
|
|
locked.append(Medal.make_locked(description, (total_downloads, 50000)))
|
|
else:
|
|
if total_downloads >= 300000:
|
|
place = 1
|
|
title = gettext(u">300k downloads")
|
|
elif total_downloads >= 100000:
|
|
place = 2
|
|
title = gettext(u">100k downloads")
|
|
elif total_downloads >= 75000:
|
|
place = 3
|
|
title = gettext(u">75k downloads")
|
|
else:
|
|
place = 10
|
|
title = gettext(u">50k downloads")
|
|
description = gettext(u"Has received %(downloads)d downloads across all packages.",
|
|
display_name=user.display_name, downloads=total_downloads)
|
|
unlocked.append(Medal.make_unlocked(place_to_color(place), "fa-users", title, description))
|
|
|
|
return unlocked, locked
|
|
|
|
|
|
@bp.route("/users/<username>/")
|
|
def profile(username):
|
|
user = User.query.filter_by(username=username).first()
|
|
if not user:
|
|
abort(404)
|
|
|
|
if not current_user.is_authenticated or (user != current_user and not current_user.can_access_todo_list()):
|
|
packages = user.packages.filter_by(state=PackageState.APPROVED)
|
|
maintained_packages = user.maintained_packages.filter_by(state=PackageState.APPROVED)
|
|
else:
|
|
packages = user.packages.filter(Package.state != PackageState.DELETED)
|
|
maintained_packages = user.maintained_packages.filter(Package.state != PackageState.DELETED)
|
|
|
|
packages = packages.order_by(db.asc(Package.title)).all()
|
|
maintained_packages = maintained_packages \
|
|
.filter(Package.author != user) \
|
|
.order_by(db.asc(Package.title)).all()
|
|
|
|
pinned_collections = user.collections.filter(Collection.private == False,
|
|
Collection.pinned == True, Collection.packages.any()).all()
|
|
|
|
unlocked, locked = get_user_medals(user)
|
|
# Process GET or invalid POST
|
|
return render_template("users/profile.html", user=user,
|
|
packages=packages, maintained_packages=maintained_packages,
|
|
medals_unlocked=unlocked, medals_locked=locked, pinned_collections=pinned_collections)
|
|
|
|
|
|
@bp.route("/users/<username>/check-forums/", methods=["POST"])
|
|
@login_required
|
|
def user_check_forums(username):
|
|
user = User.query.filter_by(username=username).first()
|
|
if user is None:
|
|
abort(404)
|
|
|
|
if current_user != user and not current_user.rank.at_least(UserRank.MODERATOR):
|
|
abort(403)
|
|
|
|
if user.forums_username is None:
|
|
abort(404)
|
|
|
|
task = check_forum_account.delay(user.forums_username, force_replace_pic=True)
|
|
next_url = url_for("users.profile", username=username)
|
|
|
|
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
|
|
|
|
|
@bp.route("/users/<username>/remove-profile-pic/", methods=["POST"])
|
|
@login_required
|
|
def user_remove_profile_pic(username):
|
|
user = User.query.filter_by(username=username).one_or_404()
|
|
if current_user != user and not current_user.rank.at_least(UserRank.MODERATOR):
|
|
abort(403)
|
|
|
|
user.profile_pic = None
|
|
db.session.commit()
|
|
return redirect(url_for("users.profile_edit", username=username))
|
|
|
|
|
|
@bp.route("/user/stats/")
|
|
@login_required
|
|
def statistics_redirect():
|
|
return redirect(url_for("users.statistics", username=current_user.username))
|
|
|
|
|
|
@bp.route("/users/<username>/stats/")
|
|
def statistics(username):
|
|
user = User.query.filter_by(username=username).first()
|
|
if user is None:
|
|
abort(404)
|
|
|
|
downloads = db.session.query(func.sum(Package.downloads)).filter(Package.author==user).one()
|
|
|
|
start = request.args.get("start")
|
|
end = request.args.get("end")
|
|
return render_template("users/stats.html", user=user, downloads=downloads[0],
|
|
start=start, end=end, options=get_daterange_options(), noindex=start or end)
|